fix(service-worker): clean up caches from old SW versions (#26319)
Since the SW immediately takes over all clients, it is safe to delete caches used by older (e.g. beta) `@angular/service-worker` versions to avoid running into browser storage quota limitations. PR Close #26319
This commit is contained in:
parent
96f38562bd
commit
00b5c7b49b
@ -123,9 +123,22 @@ export class Driver implements Debuggable, UpdateSource {
|
|||||||
// The activate event is triggered when this version of the service worker is
|
// The activate event is triggered when this version of the service worker is
|
||||||
// first activated.
|
// first activated.
|
||||||
this.scope.addEventListener('activate', (event) => {
|
this.scope.addEventListener('activate', (event) => {
|
||||||
// As above, it's safe to take over from existing clients immediately, since
|
event !.waitUntil((async() => {
|
||||||
// the new SW version will continue to serve the old application.
|
// As above, it's safe to take over from existing clients immediately, since the new SW
|
||||||
event !.waitUntil(this.scope.clients.claim());
|
// version will continue to serve the old application.
|
||||||
|
await this.scope.clients.claim();
|
||||||
|
|
||||||
|
// Once all clients have been taken over, we can delete caches used by old versions of
|
||||||
|
// `@angular/service-worker`, which are no longer needed. This can happen in the background.
|
||||||
|
this.idle.schedule('activate: cleanup-old-sw-caches', async() => {
|
||||||
|
try {
|
||||||
|
await this.cleanupOldSwCaches();
|
||||||
|
} catch (err) {
|
||||||
|
// Nothing to do - cleanup failed. Just log it.
|
||||||
|
this.debugger.log(err, 'cleanupOldSwCaches @ activate: cleanup-old-sw-caches');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})());
|
||||||
|
|
||||||
// Rather than wait for the first fetch event, which may not arrive until
|
// Rather than wait for the first fetch event, which may not arrive until
|
||||||
// the next time the application is loaded, the SW takes advantage of the
|
// the next time the application is loaded, the SW takes advantage of the
|
||||||
@ -872,6 +885,19 @@ export class Driver implements Debuggable, UpdateSource {
|
|||||||
await this.sync();
|
await this.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete caches that were used by older versions of `@angular/service-worker` to avoid running
|
||||||
|
* into storage quota limitations imposed by browsers.
|
||||||
|
* (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();
|
||||||
|
const oldSwCacheNames =
|
||||||
|
cacheNames.filter(name => /^ngsw:(?:active|staged|manifest:.+)$/.test(name));
|
||||||
|
|
||||||
|
await Promise.all(oldSwCacheNames.map(name => this.scope.caches.delete(name)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a specific version of the given resource is cached anywhere within the SW,
|
* Determine if a specific version of the given resource is cached anywhere within the SW,
|
||||||
* and fetch it if so.
|
* and fetch it if so.
|
||||||
|
@ -210,10 +210,56 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
|
|||||||
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
driver = new Driver(scope, scope, new CacheDatabase(scope, scope));
|
||||||
});
|
});
|
||||||
|
|
||||||
async_it('initializes prefetched content correctly, after activation', async() => {
|
async_it('activates without waiting', async() => {
|
||||||
expect(await scope.startup(true)).toEqual(true);
|
const skippedWaiting = await scope.startup(true);
|
||||||
|
expect(skippedWaiting).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
async_it('claims all clients, after activation', async() => {
|
||||||
|
const claimSpy = spyOn(scope.clients, 'claim');
|
||||||
|
|
||||||
|
await scope.startup(true);
|
||||||
|
expect(claimSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
async_it('cleans up old `@angular/service-worker` caches, after activation', async() => {
|
||||||
|
const claimSpy = spyOn(scope.clients, 'claim');
|
||||||
|
const cleanupOldSwCachesSpy = spyOn(driver, 'cleanupOldSwCaches');
|
||||||
|
|
||||||
|
// Automatically advance time to trigger idle tasks as they are added.
|
||||||
|
scope.autoAdvanceTime = true;
|
||||||
|
await scope.startup(true);
|
||||||
await scope.resolveSelfMessages();
|
await scope.resolveSelfMessages();
|
||||||
await driver.initialized;
|
scope.autoAdvanceTime = false;
|
||||||
|
|
||||||
|
expect(cleanupOldSwCachesSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(claimSpy).toHaveBeenCalledBefore(cleanupOldSwCachesSpy);
|
||||||
|
});
|
||||||
|
|
||||||
|
async_it(
|
||||||
|
'does not blow up if cleaning up old `@angular/service-worker` caches fails', async() => {
|
||||||
|
spyOn(driver, 'cleanupOldSwCaches').and.callFake(() => Promise.reject('Ooops'));
|
||||||
|
|
||||||
|
// Automatically advance time to trigger idle tasks as they are added.
|
||||||
|
scope.autoAdvanceTime = true;
|
||||||
|
await scope.startup(true);
|
||||||
|
await scope.resolveSelfMessages();
|
||||||
|
scope.autoAdvanceTime = false;
|
||||||
|
|
||||||
|
server.clearRequests();
|
||||||
|
|
||||||
|
expect(driver.state).toBe(DriverReadyState.NORMAL);
|
||||||
|
expect(await makeRequest(scope, '/foo.txt')).toBe('this is foo');
|
||||||
|
server.assertNoOtherRequests();
|
||||||
|
});
|
||||||
|
|
||||||
|
async_it('initializes prefetched content correctly, after activation', async() => {
|
||||||
|
// Automatically advance time to trigger idle tasks as they are added.
|
||||||
|
scope.autoAdvanceTime = true;
|
||||||
|
await scope.startup(true);
|
||||||
|
await scope.resolveSelfMessages();
|
||||||
|
scope.autoAdvanceTime = false;
|
||||||
|
|
||||||
server.assertSawRequestFor('ngsw.json');
|
server.assertSawRequestFor('ngsw.json');
|
||||||
server.assertSawRequestFor('/foo.txt');
|
server.assertSawRequestFor('/foo.txt');
|
||||||
server.assertSawRequestFor('/bar.txt');
|
server.assertSawRequestFor('/bar.txt');
|
||||||
@ -825,6 +871,41 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cleanupOldSwCaches()', () => {
|
||||||
|
async_it('should delete the correct caches', async() => {
|
||||||
|
const oldSwCacheNames = ['ngsw:active', 'ngsw:staged', 'ngsw:manifest:a1b2c3:super:duper'];
|
||||||
|
const otherCacheNames = [
|
||||||
|
'ngsuu:active',
|
||||||
|
'not:ngsw:active',
|
||||||
|
'ngsw:staged:not',
|
||||||
|
'NgSw:StAgEd',
|
||||||
|
'ngsw:manifest',
|
||||||
|
];
|
||||||
|
const allCacheNames = oldSwCacheNames.concat(otherCacheNames);
|
||||||
|
|
||||||
|
await Promise.all(allCacheNames.map(name => scope.caches.open(name)));
|
||||||
|
expect(await scope.caches.keys()).toEqual(allCacheNames);
|
||||||
|
|
||||||
|
await driver.cleanupOldSwCaches();
|
||||||
|
expect(await scope.caches.keys()).toEqual(otherCacheNames);
|
||||||
|
});
|
||||||
|
|
||||||
|
async_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')
|
||||||
|
.and.callFake(
|
||||||
|
(cacheName: string) =>
|
||||||
|
Promise.reject(`Failed to delete cache '${cacheName}'.`));
|
||||||
|
|
||||||
|
await Promise.all(oldSwCacheNames.map(name => scope.caches.open(name)));
|
||||||
|
const error = await driver.cleanupOldSwCaches().catch(err => err);
|
||||||
|
|
||||||
|
expect(error).toBe('Failed to delete cache \'ngsw:active\'.');
|
||||||
|
expect(deleteSpy).toHaveBeenCalledTimes(3);
|
||||||
|
oldSwCacheNames.forEach(name => expect(deleteSpy).toHaveBeenCalledWith(name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('bugs', () => {
|
describe('bugs', () => {
|
||||||
async_it('does not crash with bad index hash', async() => {
|
async_it('does not crash with bad index hash', async() => {
|
||||||
scope = new SwTestHarnessBuilder().withServerState(brokenServer).build();
|
scope = new SwTestHarnessBuilder().withServerState(brokenServer).build();
|
||||||
|
@ -79,6 +79,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
private skippedWaiting = true;
|
private skippedWaiting = true;
|
||||||
|
|
||||||
private selfMessageQueue: any[] = [];
|
private selfMessageQueue: any[] = [];
|
||||||
|
autoAdvanceTime = false;
|
||||||
// TODO(issue/24571): remove '!'.
|
// TODO(issue/24571): remove '!'.
|
||||||
unregistered !: boolean;
|
unregistered !: boolean;
|
||||||
readonly notifications: {title: string, options: Object}[] = [];
|
readonly notifications: {title: string, options: Object}[] = [];
|
||||||
@ -228,7 +229,7 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeout(ms: number): Promise<void> {
|
timeout(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => {
|
const promise = new Promise<void>(resolve => {
|
||||||
this.timers.push({
|
this.timers.push({
|
||||||
at: this.time + ms,
|
at: this.time + ms,
|
||||||
duration: ms,
|
duration: ms,
|
||||||
@ -236,6 +237,12 @@ export class SwTestHarness implements ServiceWorkerGlobalScope, Adapter, Context
|
|||||||
fired: false,
|
fired: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.autoAdvanceTime) {
|
||||||
|
this.advance(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
advance(by: number): void {
|
advance(by: number): void {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user