diff --git a/packages/service-worker/test/integration_spec.ts b/packages/service-worker/test/integration_spec.ts index eeee1ad2b7..68152ec565 100644 --- a/packages/service-worker/test/integration_spec.ts +++ b/packages/service-worker/test/integration_spec.ts @@ -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 => { diff --git a/packages/service-worker/worker/main.ts b/packages/service-worker/worker/main.ts index 6a600c63f0..4d380fc253 100644 --- a/packages/service-worker/worker/main.ts +++ b/packages/service-worker/worker/main.ts @@ -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)); diff --git a/packages/service-worker/worker/src/adapter.ts b/packages/service-worker/worker/src/adapter.ts index 5174ef670a..c711436d1e 100644 --- a/packages/service-worker/worker/src/adapter.ts +++ b/packages/service-worker/worker/src/adapter.ts @@ -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 { + readonly caches: NamedCacheStorage; 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}`); } /** diff --git a/packages/service-worker/worker/src/assets.ts b/packages/service-worker/worker/src/assets.ts index b511f082f2..d04b57dfeb 100644 --- a/packages/service-worker/worker/src/assets.ts +++ b/packages/service-worker/worker/src/assets.ts @@ -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; + protected cache: Promise; /** * 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, protected db: Database, - protected cacheNamePrefix: string) { + protected hashes: Map, 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 { - 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. diff --git a/packages/service-worker/worker/src/data.ts b/packages/service-worker/worker/src/data.ts index 1803571f6e..4fa3a29ff3 100644 --- a/packages/service-worker/worker/src/data.ts +++ b/packages/service-worker/worker/src/data.ts @@ -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; + private readonly cache: Promise; /** * 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 { // 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)), ]); } diff --git a/packages/service-worker/worker/src/database.ts b/packages/service-worker/worker/src/database.ts index 323024a717..0f1d3c455a 100644 --- a/packages/service-worker/worker/src/database.ts +++ b/packages/service-worker/worker/src/database.ts @@ -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. */ diff --git a/packages/service-worker/worker/src/db-cache.ts b/packages/service-worker/worker/src/db-cache.ts index 613004fd04..f360879ec4 100644 --- a/packages/service-worker/worker/src/db-cache.ts +++ b/packages/service-worker/worker/src/db-cache.ts @@ -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(); - constructor(private scope: ServiceWorkerGlobalScope, private adapter: Adapter) {} + constructor(private adapter: Adapter) {} 'delete'(name: string): Promise { 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 { 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 { 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 { 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(); }); diff --git a/packages/service-worker/worker/src/driver.ts b/packages/service-worker/worker/src/driver.ts index 401f1d3d66..895fed51b6 100644 --- a/packages/service-worker/worker/src/driver.ts +++ b/packages/service-worker/worker/src/driver.ts @@ -760,11 +760,8 @@ export class Driver implements Debuggable, UpdateSource { } private async deleteAllCaches(): Promise { - 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 { - 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))); } /** diff --git a/packages/service-worker/worker/src/named-cache-storage.ts b/packages/service-worker/worker/src/named-cache-storage.ts new file mode 100644 index 0000000000..a9ccadbf55 --- /dev/null +++ b/packages/service-worker/worker/src/named-cache-storage.ts @@ -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 implements CacheStorage { + constructor(readonly original: T, private cacheNamePrefix: string) {} + + delete(cacheName: string): Promise { + return this.original.delete(`${this.cacheNamePrefix}:${cacheName}`); + } + + has(cacheName: string): Promise { + return this.original.has(`${this.cacheNamePrefix}:${cacheName}`); + } + + async keys(): Promise { + 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 { + return this.original.match(request, options); + } + + async open(cacheName: string): Promise { + const cache = await this.original.open(`${this.cacheNamePrefix}:${cacheName}`); + return Object.assign(cache, {name: cacheName}); + } +} diff --git a/packages/service-worker/worker/src/service-worker.d.ts b/packages/service-worker/worker/src/service-worker.d.ts index 1b2a1de010..7af223bc07 100644 --- a/packages/service-worker/worker/src/service-worker.d.ts +++ b/packages/service-worker/worker/src/service-worker.d.ts @@ -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; diff --git a/packages/service-worker/worker/test/data_spec.ts b/packages/service-worker/worker/test/data_spec.ts index 448b95861b..507bbdf963 100644 --- a/packages/service-worker/worker/test/data_spec.ts +++ b/packages/service-worker/worker/test/data_spec.ts @@ -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); }); diff --git a/packages/service-worker/worker/test/happy_spec.ts b/packages/service-worker/worker/test/happy_spec.ts index 21a72fa134..21955a5de4 100644 --- a/packages/service-worker/worker/test/happy_spec.ts +++ b/packages/service-worker/worker/test/happy_spec.ts @@ -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); } diff --git a/packages/service-worker/worker/test/prefetch_spec.ts b/packages/service-worker/worker/test/prefetch_spec.ts index df8970c2eb..ceef0b5d05 100644 --- a/packages/service-worker/worker/test/prefetch_spec.ts +++ b/packages/service-worker/worker/test/prefetch_spec.ts @@ -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'); }); diff --git a/packages/service-worker/worker/testing/scope.ts b/packages/service-worker/worker/testing/scope.ts index 64f35ae45c..fb92476c3d 100644 --- a/packages/service-worker/worker/testing/scope.ts +++ b/packages/service-worker/worker/testing/scope.ts @@ -105,7 +105,8 @@ export class MockClients implements Clients { async claim(): Promise {} } -export class SwTestHarness extends Adapter implements ServiceWorkerGlobalScope, Context { +export class SwTestHarness extends Adapter implements Context, + ServiceWorkerGlobalScope { readonly clients = new MockClients(); private eventHandlers = new Map(); 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 {