refactor(service-worker): simplify accessing `CacheStorage` throughout the ServiceWorker (#42622)

This commit simplifies/systemizes accessing the `CacheStorage` through a
wrapper, with the following benefits:
- Ensuring a consistent cache name prefix is used for all caches
  (without having to repeat the prefix in different places).
- Allowing referring to caches using their name without the common
  cache name prefix.
- Exposing the cache name on cache instances, which for example makes it
  easier to delete caches without having to keep track of the name used
  to create them.

PR Close #42622
This commit is contained in:
George Kalpakas 2021-06-23 15:27:51 +03:00 committed by Jessica Janiuk
parent 73b0275dc2
commit 356dd2107b
14 changed files with 148 additions and 96 deletions

View File

@ -87,7 +87,7 @@ describe('ngsw + companion lib', () => {
mock = new MockServiceWorkerContainer();
comm = new NgswCommChannel(mock as any);
scope = new SwTestHarnessBuilder().withServerState(server).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
scope.clients.add('default');
scope.clients.getMock('default')!.queue.subscribe(msg => {

View File

@ -10,7 +10,7 @@ import {Adapter} from './src/adapter';
import {CacheDatabase} from './src/db-cache';
import {Driver} from './src/driver';
const scope = self as any as ServiceWorkerGlobalScope;
const scope = self as unknown as ServiceWorkerGlobalScope;
const adapter = new Adapter(scope.registration.scope);
const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));
const adapter = new Adapter(scope.registration.scope, self.caches);
new Driver(scope, adapter, new CacheDatabase(adapter));

View File

@ -7,6 +7,7 @@
*/
import {NormalizedUrl} from './api';
import {NamedCacheStorage} from './named-cache-storage';
/**
@ -15,20 +16,20 @@ import {NormalizedUrl} from './api';
* Mostly, this is used to mock out identifiers which are otherwise read
* from the global scope.
*/
export class Adapter {
readonly cacheNamePrefix: string;
export class Adapter<T extends CacheStorage = CacheStorage> {
readonly caches: NamedCacheStorage<T>;
private readonly origin: string;
constructor(protected readonly scopeUrl: string) {
constructor(protected readonly scopeUrl: string, caches: T) {
const parsedScopeUrl = this.parseUrl(this.scopeUrl);
// Determine the origin from the registration scope. This is used to differentiate between
// relative and absolute URLs.
this.origin = parsedScopeUrl.origin;
// Suffixing `ngsw` with the baseHref to avoid clash of cache names for SWs with different
// scopes on the same domain.
this.cacheNamePrefix = 'ngsw:' + parsedScopeUrl.path;
// Use the baseHref in the cache name prefix to avoid clash of cache names for SWs with
// different scopes on the same domain.
this.caches = new NamedCacheStorage(caches, `ngsw:${parsedScopeUrl.path}`);
}
/**

View File

@ -12,6 +12,7 @@ import {Database, Table} from './database';
import {errorToString, SwCriticalError, SwUnrecoverableStateError} from './error';
import {IdleScheduler} from './idle';
import {AssetGroupConfig} from './manifest';
import {NamedCache} from './named-cache-storage';
import {sha1Binary} from './sha1';
/**
@ -40,7 +41,7 @@ export abstract class AssetGroup {
* A Promise which resolves to the `Cache` used to back this asset group. This
* is opened from the constructor.
*/
protected cache: Promise<Cache>;
protected cache: Promise<NamedCache>;
/**
* Group name from the configuration.
@ -55,8 +56,7 @@ export abstract class AssetGroup {
constructor(
protected scope: ServiceWorkerGlobalScope, protected adapter: Adapter,
protected idle: IdleScheduler, protected config: AssetGroupConfig,
protected hashes: Map<string, string>, protected db: Database,
protected cacheNamePrefix: string) {
protected hashes: Map<string, string>, protected db: Database, cacheNamePrefix: string) {
this.name = config.name;
// Normalize the config's URLs to take the ServiceWorker's scope into account.
@ -67,8 +67,7 @@ export abstract class AssetGroup {
// This is the primary cache, which holds all of the cached requests for this group. If a
// resource isn't in this cache, it hasn't been fetched yet.
this.cache =
scope.caches.open(`${adapter.cacheNamePrefix}:${cacheNamePrefix}:${config.name}:cache`);
this.cache = adapter.caches.open(`${cacheNamePrefix}:${config.name}:cache`);
// This is the metadata table, which holds specific information for each cached URL, such as
// the timestamp of when it was added to the cache.
@ -104,9 +103,10 @@ export abstract class AssetGroup {
* Clean up all the cached data for this group.
*/
async cleanup(): Promise<void> {
await this.scope.caches.delete(
`${this.adapter.cacheNamePrefix}:${this.cacheNamePrefix}:${this.config.name}:cache`);
await this.db.delete(`${this.cacheNamePrefix}:${this.config.name}:meta`);
await Promise.all([
this.cache.then(cache => this.adapter.caches.delete(cache.name)),
this.metadata.then(metadata => this.db.delete(metadata.name)),
]);
}
/**
@ -138,11 +138,9 @@ export abstract class AssetGroup {
// This resource has no hash, and yet exists in the cache. Check how old this request is
// to make sure it's still usable.
if (await this.needToRevalidate(req, cachedResponse)) {
this.idle.schedule(
`revalidate(${this.cacheNamePrefix}, ${this.config.name}): ${req.url}`,
async () => {
await this.fetchAndCacheOnce(req);
});
this.idle.schedule(`revalidate(${cache.name}): ${req.url}`, async () => {
await this.fetchAndCacheOnce(req);
});
}
// In either case (revalidation or not), the cached response must be good.

View File

@ -10,6 +10,7 @@ import {Adapter, Context} from './adapter';
import {Database, Table} from './database';
import {DebugHandler} from './debug';
import {DataGroupConfig} from './manifest';
import {NamedCache} from './named-cache-storage';
/**
* A metadata record of how old a particular cached resource is.
@ -227,7 +228,7 @@ export class DataGroup {
/**
* The `Cache` instance in which resources belonging to this group are cached.
*/
private readonly cache: Promise<Cache>;
private readonly cache: Promise<NamedCache>;
/**
* Tracks the LRU state of resources in this cache.
@ -247,10 +248,9 @@ export class DataGroup {
constructor(
private scope: ServiceWorkerGlobalScope, private adapter: Adapter,
private config: DataGroupConfig, private db: Database, private debugHandler: DebugHandler,
private cacheNamePrefix: string) {
cacheNamePrefix: string) {
this.patterns = config.patterns.map(pattern => new RegExp(pattern));
this.cache =
scope.caches.open(`${adapter.cacheNamePrefix}:${cacheNamePrefix}:${config.name}:cache`);
this.cache = adapter.caches.open(`${cacheNamePrefix}:${config.name}:cache`);
this.lruTable = this.db.open(`${cacheNamePrefix}:${config.name}:lru`, config.cacheQueryOptions);
this.ageTable = this.db.open(`${cacheNamePrefix}:${config.name}:age`, config.cacheQueryOptions);
}
@ -549,10 +549,9 @@ export class DataGroup {
async cleanup(): Promise<void> {
// Remove both the cache and the database entries which track LRU stats.
await Promise.all([
this.scope.caches.delete(
`${this.adapter.cacheNamePrefix}:${this.cacheNamePrefix}:${this.config.name}:cache`),
this.db.delete(`${this.cacheNamePrefix}:${this.config.name}:age`),
this.db.delete(`${this.cacheNamePrefix}:${this.config.name}:lru`),
this.cache.then(cache => this.adapter.caches.delete(cache.name)),
this.ageTable.then(table => this.db.delete(table.name)),
this.lruTable.then(table => this.db.delete(table.name)),
]);
}

View File

@ -10,6 +10,11 @@
* An abstract table, with the ability to read/write objects stored under keys.
*/
export interface Table {
/**
* The name of this table in the database.
*/
name: string;
/**
* Delete a key from the table.
*/

View File

@ -15,21 +15,21 @@ import {Database, NotFound, Table} from './database';
* state within mock `Response` objects.
*/
export class CacheDatabase implements Database {
private cacheNamePrefix = `${this.adapter.cacheNamePrefix}:db`;
private cacheNamePrefix = 'db';
private tables = new Map<string, CacheTable>();
constructor(private scope: ServiceWorkerGlobalScope, private adapter: Adapter) {}
constructor(private adapter: Adapter) {}
'delete'(name: string): Promise<boolean> {
if (this.tables.has(name)) {
this.tables.delete(name);
}
return this.scope.caches.delete(`${this.cacheNamePrefix}:${name}`);
return this.adapter.caches.delete(`${this.cacheNamePrefix}:${name}`);
}
async list(): Promise<string[]> {
const prefix = `${this.cacheNamePrefix}:`;
const allCacheNames = await this.scope.caches.keys();
const allCacheNames = await this.adapter.caches.keys();
const dbCacheNames = allCacheNames.filter(name => name.startsWith(prefix));
// Return the un-prefixed table names, so they can be used with other `CacheDatabase` methods
@ -39,7 +39,7 @@ export class CacheDatabase implements Database {
async open(name: string, cacheQueryOptions?: CacheQueryOptions): Promise<Table> {
if (!this.tables.has(name)) {
const cache = await this.scope.caches.open(`${this.cacheNamePrefix}:${name}`);
const cache = await this.adapter.caches.open(`${this.cacheNamePrefix}:${name}`);
const table = new CacheTable(name, cache, this.adapter, cacheQueryOptions);
this.tables.set(name, table);
}
@ -52,7 +52,7 @@ export class CacheDatabase implements Database {
*/
export class CacheTable implements Table {
constructor(
readonly table: string, private cache: Cache, private adapter: Adapter,
readonly name: string, private cache: Cache, private adapter: Adapter,
private cacheQueryOptions?: CacheQueryOptions) {}
private request(key: string): Request {
@ -70,7 +70,7 @@ export class CacheTable implements Table {
read(key: string): Promise<any> {
return this.cache.match(this.request(key), this.cacheQueryOptions).then(res => {
if (res === undefined) {
return Promise.reject(new NotFound(this.table, key));
return Promise.reject(new NotFound(this.name, key));
}
return res.json();
});

View File

@ -760,11 +760,8 @@ export class Driver implements Debuggable, UpdateSource {
}
private async deleteAllCaches(): Promise<void> {
const cacheNames = await this.scope.caches.keys();
const ownCacheNames =
cacheNames.filter(name => name.startsWith(`${this.adapter.cacheNamePrefix}:`));
await Promise.all(ownCacheNames.map(name => this.scope.caches.delete(name)));
const cacheNames = await this.adapter.caches.keys();
await Promise.all(cacheNames.map(name => this.adapter.caches.delete(name)));
}
/**
@ -988,9 +985,13 @@ export class Driver implements Debuggable, UpdateSource {
* (Since at this point the SW has claimed all clients, it is safe to remove those caches.)
*/
async cleanupOldSwCaches(): Promise<void> {
const cacheNames = await this.scope.caches.keys();
// This is an exceptional case, where we need to interact with caches that would not be
// generated by this ServiceWorker (but by old versions of it). Use the native `CacheStorage`
// directly.
const caches = this.adapter.caches.original;
const cacheNames = await caches.keys();
const oldSwCacheNames = cacheNames.filter(name => /^ngsw:(?!\/)/.test(name));
await Promise.all(oldSwCacheNames.map(name => this.scope.caches.delete(name)));
await Promise.all(oldSwCacheNames.map(name => caches.delete(name)));
}
/**

View File

@ -0,0 +1,45 @@
/**
* @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
*/
export interface NamedCache extends Cache {
readonly name: string;
}
/**
* A wrapper around `CacheStorage` to allow interacting with caches more easily and consistently by:
* - Adding a `name` property to all opened caches, which can be used to easily perform other
* operations that require the cache name.
* - Name-spacing cache names to avoid conflicts with other caches on the same domain.
*/
export class NamedCacheStorage<T extends CacheStorage> implements CacheStorage {
constructor(readonly original: T, private cacheNamePrefix: string) {}
delete(cacheName: string): Promise<boolean> {
return this.original.delete(`${this.cacheNamePrefix}:${cacheName}`);
}
has(cacheName: string): Promise<boolean> {
return this.original.has(`${this.cacheNamePrefix}:${cacheName}`);
}
async keys(): Promise<string[]> {
const prefix = `${this.cacheNamePrefix}:`;
const allCacheNames = await this.original.keys();
const ownCacheNames = allCacheNames.filter(name => name.startsWith(prefix));
return ownCacheNames.map(name => name.slice(prefix.length));
}
match(request: RequestInfo, options?: MultiCacheQueryOptions): Promise<Response|undefined> {
return this.original.match(request, options);
}
async open(cacheName: string): Promise<NamedCache> {
const cache = await this.original.open(`${this.cacheNamePrefix}:${cacheName}`);
return Object.assign(cache, {name: cacheName});
}
}

View File

@ -111,7 +111,9 @@ interface ExtendableMessageEvent extends ExtendableEvent {
// ServiceWorkerGlobalScope
interface ServiceWorkerGlobalScope {
caches: CacheStorage;
// Intentionally does not include a `caches` property to disallow accessing `CacheStorage` APIs
// directly. All interactions with `CacheStorage` should go through a `NamedCacheStorage` instance
// (exposed by the `Adapter`).
clients: Clients;
registration: ServiceWorkerRegistration;

View File

@ -126,7 +126,7 @@ describe('data cache', () => {
let driver: Driver;
beforeEach(async () => {
scope = new SwTestHarnessBuilder().withServerState(server).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
// Initialize.
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
@ -144,7 +144,7 @@ describe('data cache', () => {
describe('in performance mode', () => {
it('names the caches correctly', async () => {
expect(await makeRequest(scope, '/api/test')).toEqual('version 1');
const keys = await scope.caches.keys();
const keys = await scope.caches.original.keys();
expect(keys.every(key => key.startsWith('ngsw:/:'))).toEqual(true);
});

View File

@ -304,7 +304,7 @@ describe('Driver', () => {
brokenServer.reset();
scope = new SwTestHarnessBuilder().withServerState(server).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
});
it('activates without waiting', async () => {
@ -553,10 +553,10 @@ describe('Driver', () => {
await driver.initialized;
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withCacheState(scope.caches.original.dehydrate())
.withServerState(serverUpdate)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
serverUpdate.assertNoOtherRequests();
@ -609,10 +609,10 @@ describe('Driver', () => {
serverUpdate.clearRequests();
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withCacheState(scope.caches.original.dehydrate())
.withServerState(serverUpdate)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
expect(await makeRequest(scope, '/foo.txt', 'new')).toEqual('this is foo v2');
@ -671,16 +671,16 @@ describe('Driver', () => {
await driver.initialized;
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withCacheState(scope.caches.original.dehydrate())
.withServerState(serverUpdate)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
serverUpdate.assertNoOtherRequests();
let keys = await scope.caches.keys();
let hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`));
let hasOriginalCaches = keys.some(name => name.startsWith(`${manifestHash}:`));
expect(hasOriginalCaches).toEqual(true);
scope.clients.remove('default');
@ -689,11 +689,11 @@ describe('Driver', () => {
await driver.idle.empty;
serverUpdate.clearRequests();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo v2');
keys = await scope.caches.keys();
hasOriginalCaches = keys.some(name => name.startsWith(`ngsw:/:${manifestHash}:`));
hasOriginalCaches = keys.some(name => name.startsWith(`${manifestHash}:`));
expect(hasOriginalCaches).toEqual(false);
});
@ -1255,7 +1255,7 @@ describe('Driver', () => {
it('should show debug info when the scope is not root', async () => {
const newScope =
new SwTestHarnessBuilder('http://localhost/foo/bar/').withServerState(server).build();
new Driver(newScope, newScope, new CacheDatabase(newScope, newScope));
new Driver(newScope, newScope, new CacheDatabase(newScope));
expect(await makeRequest(newScope, '/foo/bar/ngsw/state'))
.toMatch(/^NGSW Debug Info:\n\nDriver version: .+\nDriver state: NORMAL/);
@ -1324,7 +1324,8 @@ describe('Driver', () => {
});
const getClientAssignments = async (sw: SwTestHarness, baseHref: string) => {
const cache = await sw.caches.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache;
const cache =
await sw.caches.original.open(`ngsw:${baseHref}:db:control`) as unknown as MockCache;
const dehydrated = cache.dehydrate();
return JSON.parse(dehydrated['/assignments'].body!) as any;
};
@ -1344,7 +1345,7 @@ describe('Driver', () => {
.withCacheState(initialCacheState)
.withServerState(serverState)
.build();
const newDriver = new Driver(newScope, newScope, new CacheDatabase(newScope, newScope));
const newDriver = new Driver(newScope, newScope, new CacheDatabase(newScope));
await makeRequest(newScope, newManifest.index, baseHref.replace(/\//g, '_'));
await newDriver.initialized;
@ -1359,14 +1360,14 @@ describe('Driver', () => {
it('includes the SW scope in all cache names', async () => {
// SW with scope `/`.
const [rootScope, rootManifestHash] = await initializeSwFor('/');
const cacheNames = await rootScope.caches.keys();
const cacheNames = await rootScope.caches.original.keys();
expect(cacheNames).toEqual(cacheKeysFor('/', rootManifestHash));
expect(cacheNames.every(name => name.includes('/'))).toBe(true);
// SW with scope `/foo/`.
const [fooScope, fooManifestHash] = await initializeSwFor('/foo/');
const fooCacheNames = await fooScope.caches.keys();
const fooCacheNames = await fooScope.caches.original.keys();
expect(fooCacheNames).toEqual(cacheKeysFor('/foo/', fooManifestHash));
expect(fooCacheNames.every(name => name.includes('/foo/'))).toBe(true);
@ -1381,8 +1382,8 @@ describe('Driver', () => {
// Add new SW with different scope.
const [barScope, barManifestHash] =
await initializeSwFor('/bar/', await fooScope.caches.dehydrate());
const barCacheNames = await barScope.caches.keys();
await initializeSwFor('/bar/', await fooScope.caches.original.dehydrate());
const barCacheNames = await barScope.caches.original.keys();
const barAssignments = await getClientAssignments(barScope, '/bar/');
expect(barAssignments).toEqual({_bar_: barManifestHash});
@ -1412,7 +1413,7 @@ describe('Driver', () => {
// Add new SW with same scope.
const [fooScope2, fooManifestHash2] =
await initializeSwFor('/foo/', await fooScope.caches.dehydrate());
await initializeSwFor('/foo/', await fooScope.caches.original.dehydrate());
// Update client `_foo_` but not client `_bar_`.
await fooScope2.handleMessage({action: 'CHECK_FOR_UPDATES'}, '_foo_');
@ -1490,9 +1491,9 @@ describe('Driver', () => {
expect(await makeRequest(scope, '/unhashed/a.txt')).toEqual('this is unhashed');
server.clearRequests();
const state = scope.caches.dehydrate();
const state = scope.caches.original.dehydrate();
scope = new SwTestHarnessBuilder().withCacheState(state).withServerState(server).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
server.assertNoRequestFor('/unhashed/a.txt');
@ -1514,10 +1515,10 @@ describe('Driver', () => {
server.clearRequests();
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withCacheState(scope.caches.original.dehydrate())
.withServerState(serverUpdate)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
await driver.initialized;
@ -1709,7 +1710,7 @@ describe('Driver', () => {
scope = new SwTestHarnessBuilder('http://localhost/base/href/')
.withServerState(serverWithBaseHref)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
});
it('initializes prefetched content correctly, after a request kicks it off', async () => {
@ -1820,21 +1821,21 @@ describe('Driver', () => {
];
const allCacheNames = oldSwCacheNames.concat(otherCacheNames);
await Promise.all(allCacheNames.map(name => scope.caches.open(name)));
expect(await scope.caches.keys()).toEqual(allCacheNames);
await Promise.all(allCacheNames.map(name => scope.caches.original.open(name)));
expect(await scope.caches.original.keys()).toEqual(allCacheNames);
await driver.cleanupOldSwCaches();
expect(await scope.caches.keys()).toEqual(otherCacheNames);
expect(await scope.caches.original.keys()).toEqual(otherCacheNames);
});
it('should delete other caches even if deleting one of them fails', async () => {
const oldSwCacheNames = ['ngsw:active', 'ngsw:staged', 'ngsw:manifest:a1b2c3:super:duper'];
const deleteSpy =
spyOn(scope.caches, 'delete')
spyOn(scope.caches.original, 'delete')
.and.callFake(
(cacheName: string) => Promise.reject(`Failed to delete cache '${cacheName}'.`));
await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name)));
await Promise.all(oldSwCacheNames.map(name => scope.caches.original.open(name)));
const error = await driver.cleanupOldSwCaches().catch(err => err);
expect(error).toBe('Failed to delete cache \'ngsw:active\'.');
@ -1847,7 +1848,7 @@ describe('Driver', () => {
it('does not crash with bad index hash', async () => {
scope = new SwTestHarnessBuilder().withServerState(brokenServer).build();
(scope.registration as any).scope = 'http://site.com';
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo (broken)');
});
@ -1858,10 +1859,10 @@ describe('Driver', () => {
server.clearRequests();
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withCacheState(scope.caches.original.dehydrate())
.withServerState(brokenServer)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
await driver.checkForUpdate();
scope.advance(12000);
@ -2018,7 +2019,7 @@ describe('Driver', () => {
expect(driver.state).toBe(DriverReadyState.NORMAL);
// Ensure the data has been stored in the DB.
const db: MockCache = await scope.caches.open('ngsw:/:db:control') as any;
const db: MockCache = await scope.caches.open('db:control') as any;
const getLatestHashFromDb = async () => (await (await db.match('/latest')).json()).latest;
expect(await getLatestHashFromDb()).toBe(manifestHash);
@ -2145,7 +2146,7 @@ describe('Driver', () => {
// Create initial server state and initialize the SW.
scope = new SwTestHarnessBuilder().withServerState(serverState1).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
// Verify that all three clients are able to make the request.
expect(await makeRequest(scope, '/foo.hash.js', 'client1')).toBe('console.log("FOO");');
@ -2223,7 +2224,7 @@ describe('Driver', () => {
// Create initial server state and initialize the SW.
scope = new SwTestHarnessBuilder().withServerState(originalServer).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
await driver.initialized;
@ -2236,10 +2237,10 @@ describe('Driver', () => {
// Update the server state to emulate deploying a new version (where `foo.hash.js` does not
// exist any more). Keep the cache though.
scope = new SwTestHarnessBuilder()
.withCacheState(scope.caches.dehydrate())
.withCacheState(scope.caches.original.dehydrate())
.withServerState(updatedServer)
.build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
// The SW is still able to serve `foo.hash.js` from the cache.
expect(await makeRequest(scope, '/foo.hash.js')).toBe('console.log("FOO");');
@ -2267,7 +2268,7 @@ describe('Driver', () => {
.build();
scope = new SwTestHarnessBuilder().withServerState(serverV5).build();
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
driver = new Driver(scope, scope, new CacheDatabase(scope));
});
// Test this bug: https://github.com/angular/angular/issues/27209
@ -2321,7 +2322,7 @@ describe('Driver', () => {
const freshnessManifest: Manifest = {...manifest, navigationRequestStrategy: 'freshness'};
const server = serverBuilderBase.withManifest(freshnessManifest).build();
const scope = new SwTestHarnessBuilder().withServerState(server).build();
const driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
const driver = new Driver(scope, scope, new CacheDatabase(scope));
return {server, scope, driver};
}
@ -2333,8 +2334,7 @@ async function removeAssetFromCache(
scope: SwTestHarness, appVersionManifest: Manifest, assetPath: string) {
const assetGroupName =
appVersionManifest.assetGroups?.find(group => group.urls.includes(assetPath))?.name;
const cacheName = `${scope.cacheNamePrefix}:${sha1(JSON.stringify(appVersionManifest))}:assets:${
assetGroupName}:cache`;
const cacheName = `${sha1(JSON.stringify(appVersionManifest))}:assets:${assetGroupName}:cache`;
const cache = await scope.caches.open(cacheName);
return cache.delete(assetPath);
}

View File

@ -31,7 +31,7 @@ const server = new MockServerStateBuilder().withStaticFiles(dist).withManifest(m
const scope = new SwTestHarnessBuilder().withServerState(server).build();
const db = new CacheDatabase(scope, scope);
const db = new CacheDatabase(scope);
describe('prefetch assets', () => {
@ -57,10 +57,11 @@ describe('prefetch assets', () => {
});
it('persists the cache across restarts', async () => {
await group.initializeFully();
const freshScope = new SwTestHarnessBuilder().withCacheState(scope.caches.dehydrate()).build();
const freshScope =
new SwTestHarnessBuilder().withCacheState(scope.caches.original.dehydrate()).build();
group = new PrefetchAssetGroup(
freshScope, freshScope, idle, manifest.assetGroups![0], tmpHashTable(manifest),
new CacheDatabase(freshScope, freshScope), 'test');
new CacheDatabase(freshScope), 'test');
await group.initializeFully();
const res1 = await group.handleFetch(scope.newRequest('/foo.txt'), scope);
const res2 = await group.handleFetch(scope.newRequest('/bar.txt'), scope);
@ -82,7 +83,7 @@ describe('prefetch assets', () => {
const badScope = new SwTestHarnessBuilder().withServerState(badServer).build();
group = new PrefetchAssetGroup(
badScope, badScope, idle, manifest.assetGroups![0], tmpHashTable(manifest),
new CacheDatabase(badScope, badScope), 'test');
new CacheDatabase(badScope), 'test');
const err = await errorFrom(group.initializeFully());
expect(err.message).toContain('Hash mismatch');
});

View File

@ -105,7 +105,8 @@ export class MockClients implements Clients {
async claim(): Promise<any> {}
}
export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope, Context {
export class SwTestHarness extends Adapter<MockCacheStorage> implements Context,
ServiceWorkerGlobalScope {
readonly clients = new MockClients();
private eventHandlers = new Map<string, Function>();
private skippedWaiting = false;
@ -163,9 +164,8 @@ export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope,
parseUrl = parseUrl;
constructor(
private server: MockServerState, readonly caches: MockCacheStorage, scopeUrl: string) {
super(scopeUrl);
constructor(private server: MockServerState, caches: MockCacheStorage, scopeUrl: string) {
super(scopeUrl, caches);
}
async resolveSelfMessages(): Promise<void> {