fix(service-worker): detect new version even if files are identical to an old one (#26006)

Previously, if an app version contained the same files as an older
version (e.g. making a change, then rolling it back), the SW would not
detect it as the latest version (and update clients).

This commit fixes it by adding a `timestamp` field in `ngsw.json`, which
makes each build unique (with sufficiently high probability).

Fixes #24338

PR Close #26006
This commit is contained in:
George Kalpakas 2019-03-05 15:24:07 +02:00 committed by Andrew Kushnir
parent 5fded9fcc8
commit 586234bb01
8 changed files with 44 additions and 7 deletions

View File

@ -32,6 +32,7 @@ export class Generator {
return { return {
configVersion: 1, configVersion: 1,
timestamp: Date.now(),
appData: config.appData, appData: config.appData,
index: joinUrls(this.baseHref, config.index), assetGroups, index: joinUrls(this.baseHref, config.index), assetGroups,
dataGroups: this.processDataGroups(config), dataGroups: this.processDataGroups(config),

View File

@ -10,6 +10,8 @@ import {Generator} from '../src/generator';
import {MockFilesystem} from '../testing/mock'; import {MockFilesystem} from '../testing/mock';
describe('Generator', () => { describe('Generator', () => {
beforeEach(() => spyOn(Date, 'now').and.returnValue(1234567890123));
it('generates a correct config', done => { it('generates a correct config', done => {
const fs = new MockFilesystem({ const fs = new MockFilesystem({
'/index.html': 'This is a test', '/index.html': 'This is a test',
@ -70,6 +72,7 @@ describe('Generator', () => {
res.then(config => { res.then(config => {
expect(config).toEqual({ expect(config).toEqual({
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
appData: { appData: {
test: true, test: true,
}, },
@ -137,6 +140,7 @@ describe('Generator', () => {
res.then(config => { res.then(config => {
expect(config).toEqual({ expect(config).toEqual({
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
appData: undefined, appData: undefined,
index: '/test/index.html', index: '/test/index.html',
assetGroups: [], assetGroups: [],

View File

@ -31,6 +31,7 @@ function obsToSinglePromise<T>(obs: Observable<T>): Promise<T> {
const manifest: Manifest = { const manifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
appData: {version: '1'}, appData: {version: '1'},
index: '/only.txt', index: '/only.txt',
assetGroups: [{ assetGroups: [{
@ -46,6 +47,7 @@ const manifest: Manifest = {
const manifestUpdate: Manifest = { const manifestUpdate: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
appData: {version: '2'}, appData: {version: '2'},
index: '/only.txt', index: '/only.txt',
assetGroups: [{ assetGroups: [{

View File

@ -12,6 +12,7 @@ export type ManifestHash = string;
export interface Manifest { export interface Manifest {
configVersion: number; configVersion: number;
timestamp: number;
appData?: {[key: string]: string}; appData?: {[key: string]: string};
index: string; index: string;
assetGroups?: AssetGroupConfig[]; assetGroups?: AssetGroupConfig[];

View File

@ -39,6 +39,7 @@ const distUpdate = new MockFileSystemBuilder()
const manifest: Manifest = { const manifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
index: '/index.html', index: '/index.html',
assetGroups: [ assetGroups: [
{ {

View File

@ -51,6 +51,7 @@ const brokenFs = new MockFileSystemBuilder().addFile('/foo.txt', 'this is foo').
const brokenManifest: Manifest = { const brokenManifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
index: '/foo.txt', index: '/foo.txt',
assetGroups: [{ assetGroups: [{
name: 'assets', name: 'assets',
@ -86,6 +87,7 @@ const manifestOld: ManifestV5 = {
const manifest: Manifest = { const manifest: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
appData: { appData: {
version: 'original', version: 'original',
}, },
@ -133,6 +135,7 @@ const manifest: Manifest = {
const manifestUpdate: Manifest = { const manifestUpdate: Manifest = {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
appData: { appData: {
version: 'update', version: 'update',
}, },
@ -185,12 +188,16 @@ const manifestUpdate: Manifest = {
hashTable: tmpHashTableForFs(distUpdate), hashTable: tmpHashTableForFs(distUpdate),
}; };
const server = new MockServerStateBuilder() const serverBuilderBase =
new MockServerStateBuilder()
.withStaticFiles(dist) .withStaticFiles(dist)
.withManifest(manifest)
.withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect') .withRedirect('/redirected.txt', '/redirect-target.txt', 'this was a redirect')
.withError('/error.txt') .withError('/error.txt');
.build();
const server = serverBuilderBase.withManifest(manifest).build();
const serverRollback =
serverBuilderBase.withManifest({...manifest, timestamp: manifest.timestamp + 1}).build();
const serverUpdate = const serverUpdate =
new MockServerStateBuilder() new MockServerStateBuilder()
@ -372,6 +379,19 @@ const manifestUpdateHash = sha1(JSON.stringify(manifestUpdate));
serverUpdate.assertNoOtherRequests(); serverUpdate.assertNoOtherRequests();
}); });
async_it('detects new version even if only `manifest.timestamp` is different', async() => {
expect(await makeRequest(scope, '/foo.txt', 'newClient')).toEqual('this is foo');
await driver.initialized;
scope.updateServerState(serverUpdate);
expect(await driver.checkForUpdate()).toEqual(true);
expect(await makeRequest(scope, '/foo.txt', 'newerClient')).toEqual('this is foo v2');
scope.updateServerState(serverRollback);
expect(await driver.checkForUpdate()).toEqual(true);
expect(await makeRequest(scope, '/foo.txt', 'newestClient')).toEqual('this is foo');
});
async_it('updates a specific client to new content on request', async() => { async_it('updates a specific client to new content on request', 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;

View File

@ -88,7 +88,13 @@ export class MockServerStateBuilder {
return this; return this;
} }
build(): MockServerState { return new MockServerState(this.resources, this.errors); } build(): MockServerState {
// Take a "snapshot" of the current `resources` and `errors`.
const resources = new Map(this.resources.entries());
const errors = new Set(this.errors.values());
return new MockServerState(resources, errors);
}
} }
export class MockServerState { export class MockServerState {
@ -187,6 +193,7 @@ export function tmpManifestSingleAssetGroup(fs: MockFileSystem): Manifest {
files.forEach(path => { hashTable[path] = fs.lookup(path) !.hash; }); files.forEach(path => { hashTable[path] = fs.lookup(path) !.hash; });
return { return {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
index: '/index.html', index: '/index.html',
assetGroups: [ assetGroups: [
{ {

View File

@ -306,6 +306,7 @@ export class ConfigBuilder {
const hashTable = {}; const hashTable = {};
return { return {
configVersion: 1, configVersion: 1,
timestamp: 1234567890123,
index: '/index.html', assetGroups, index: '/index.html', assetGroups,
navigationUrls: [], hashTable, navigationUrls: [], hashTable,
}; };