fix(service-worker): check for updates on navigation
Currently the Service Worker checks for updates only on SW startup, an event which happens frequently but also nondeterministically. This makes it hard for developers to observe the update process or reason about how updates will be delivered to users. This problem is exacerbated by the DevTools behavior of keeping the SW alive indefinitely while opened, effectively preventing the page from updating at all. This change causes the SW to additionally check for updates on navigation requests (app page reloads). This creates deterministic update behavior, and is much easier for developers to reason about. It does leave the old update-on-SW-startup behavior in place, as removing that would be a breaking change. Fixes #20877
This commit is contained in:
parent
0b2d636b75
commit
20e1cc049f
|
@ -32,12 +32,10 @@ server can ensure that the Angular app always has a consistent set of files.
|
||||||
|
|
||||||
#### Update checks
|
#### Update checks
|
||||||
|
|
||||||
Every time the Angular service worker starts, it checks for updates to the
|
Every time the user opens or refreshes the application, the Angular service worker
|
||||||
app by looking for updates to the `ngsw.json` manifest.
|
checks for updates to the app by looking for updates to the `ngsw.json` manifest. If
|
||||||
|
an update is found, it is downloaded and cached automatically, and will be served
|
||||||
Note that the service worker starts periodically throughout the usage of
|
the next time the application is loaded.
|
||||||
the app because the web browser terminates the service worker if the page
|
|
||||||
is idle beyond a given timeout.
|
|
||||||
|
|
||||||
### Resource integrity
|
### Resource integrity
|
||||||
|
|
||||||
|
@ -276,8 +274,8 @@ with service workers. Such tools can be powerful when used properly,
|
||||||
but there are a few things to keep in mind.
|
but there are a few things to keep in mind.
|
||||||
|
|
||||||
* When using developer tools, the service worker is kept running
|
* When using developer tools, the service worker is kept running
|
||||||
in the background and never restarts. For the Angular service
|
in the background and never restarts. This can cause behavior with Dev
|
||||||
worker, this means that update checks to the app will generally not happen.
|
Tools open to differ from behavior a user might experience.
|
||||||
|
|
||||||
* If you look in the Cache Storage viewer, the cache is frequently
|
* If you look in the Cache Storage viewer, the cache is frequently
|
||||||
out of date. Right click the Cache Storage title and refresh the caches.
|
out of date. Right click the Cache Storage title and refresh the caches.
|
||||||
|
|
|
@ -88,6 +88,11 @@ export class Driver implements Debuggable, UpdateSource {
|
||||||
|
|
||||||
private lastUpdateCheck: number|null = null;
|
private lastUpdateCheck: number|null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether there is a check for updates currently scheduled due to navigation.
|
||||||
|
*/
|
||||||
|
private scheduledNavUpdateCheck: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A scheduler which manages a queue of tasks that need to be executed when the SW is
|
* A scheduler which manages a queue of tasks that need to be executed when the SW is
|
||||||
* not doing any other work (not processing any other requests).
|
* not doing any other work (not processing any other requests).
|
||||||
|
@ -327,6 +332,15 @@ export class Driver implements Debuggable, UpdateSource {
|
||||||
return this.safeFetch(event.request);
|
return this.safeFetch(event.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On navigation requests, check for new updates.
|
||||||
|
if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
|
||||||
|
this.scheduledNavUpdateCheck = true;
|
||||||
|
this.idle.schedule('check-updates-on-navigation', async() => {
|
||||||
|
this.scheduledNavUpdateCheck = false;
|
||||||
|
await this.checkForUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Decide which version of the app to use to serve this request. This is asynchronous as in
|
// Decide which version of the app to use to serve this request. This is asynchronous as in
|
||||||
// some cases, a record will need to be written to disk about the assignment that is made.
|
// some cases, a record will need to be written to disk about the assignment that is made.
|
||||||
const appVersion = await this.assignVersion(event);
|
const appVersion = await this.assignVersion(event);
|
||||||
|
|
|
@ -337,6 +337,41 @@ export function main() {
|
||||||
serverUpdate.assertNoOtherRequests();
|
serverUpdate.assertNoOtherRequests();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async_it('checks for updates on navigation', async() => {
|
||||||
|
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||||
|
await driver.initialized;
|
||||||
|
server.clearRequests();
|
||||||
|
|
||||||
|
expect(await makeRequest(scope, '/foo.txt', 'default', {
|
||||||
|
mode: 'navigate',
|
||||||
|
})).toEqual('this is foo');
|
||||||
|
|
||||||
|
scope.advance(12000);
|
||||||
|
await driver.idle.empty;
|
||||||
|
|
||||||
|
server.assertSawRequestFor('ngsw.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
async_it('does not make concurrent checks for updates on navigation', async() => {
|
||||||
|
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||||
|
await driver.initialized;
|
||||||
|
server.clearRequests();
|
||||||
|
|
||||||
|
expect(await makeRequest(scope, '/foo.txt', 'default', {
|
||||||
|
mode: 'navigate',
|
||||||
|
})).toEqual('this is foo');
|
||||||
|
|
||||||
|
expect(await makeRequest(scope, '/foo.txt', 'default', {
|
||||||
|
mode: 'navigate',
|
||||||
|
})).toEqual('this is foo');
|
||||||
|
|
||||||
|
scope.advance(12000);
|
||||||
|
await driver.idle.empty;
|
||||||
|
|
||||||
|
server.assertSawRequestFor('ngsw.json');
|
||||||
|
server.assertNoOtherRequests();
|
||||||
|
});
|
||||||
|
|
||||||
async_it('preserves multiple client assignments across restarts', async() => {
|
async_it('preserves multiple client assignments across restarts', async() => {
|
||||||
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
|
||||||
await driver.initialized;
|
await driver.initialized;
|
||||||
|
|
Loading…
Reference in New Issue