fix(docs-infra): support recovering from unrecoverable SW states (#39651)
Occasionally, the SW would end up in a broken state where some of the eagerly cached resources of an older version were available in the local cache, but others (such as lazy-loaded bundles) were not. This would leave the app in a broken state and a blank screen would be displayed. See #28114 for a more detailed discussion. This commit takes advantage of the newly introduced (in v11) [SwUpdate#unrecoverable][1] API to detect these bad states and recover by doing a full page reload whenever an [UnrecoverableStateEvent][2] is emitted. Partially addresses #28114. NOTE: Currently, `SwUpdate.unrecoverable` only works if the app has already bootstrapped; i.e. if only lazy-loaded bundles have been purged from the cache. That should be fine in practice, since the cache entries are removed in least-recently-used order. Thus the eagerly loaded bundles will be the last to be removed from the cache (which rarely happens in practice). [1]: https://v11.angular.io/api/service-worker/SwUpdate#unrecoverable [2]: https://v11.angular.io/api/service-worker/UnrecoverableStateEvent PR Close #39651
This commit is contained in:
parent
305d05545a
commit
935cf433ed
|
@ -70,6 +70,10 @@ export class LocationService {
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadPage(): void {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
private stripSlashes(url: string) {
|
private stripSlashes(url: string) {
|
||||||
return url.replace(/^\/+/, '').replace(/\/+(\?|#|$)/, '$1');
|
return url.replace(/^\/+/, '').replace(/\/+(\?|#|$)/, '$1');
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,16 @@ describe('SwUpdatesService', () => {
|
||||||
expect(location.fullPageNavigationNeeded).toHaveBeenCalledTimes(2);
|
expect(location.fullPageNavigationNeeded).toHaveBeenCalledTimes(2);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should request a page reload when an unrecoverable state has been detected', run(() => {
|
||||||
|
expect(location.reloadPage).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
swu.$$unrecoverableSubj.next({reason: 'Something bad happened'});
|
||||||
|
expect(location.reloadPage).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
swu.$$unrecoverableSubj.next({reason: 'Something worse happened'});
|
||||||
|
expect(location.reloadPage).toHaveBeenCalledTimes(2);
|
||||||
|
}));
|
||||||
|
|
||||||
describe('when `SwUpdate` is not enabled', () => {
|
describe('when `SwUpdate` is not enabled', () => {
|
||||||
const runDeactivated = (specFn: VoidFunction) => run(specFn, false);
|
const runDeactivated = (specFn: VoidFunction) => run(specFn, false);
|
||||||
|
|
||||||
|
@ -150,6 +160,13 @@ describe('SwUpdatesService', () => {
|
||||||
|
|
||||||
expect(location.fullPageNavigationNeeded).not.toHaveBeenCalled();
|
expect(location.fullPageNavigationNeeded).not.toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should never request a page reload', runDeactivated(() => {
|
||||||
|
swu.$$unrecoverableSubj.next({reason: 'Something bad happened'});
|
||||||
|
swu.$$unrecoverableSubj.next({reason: 'Something worse happened'});
|
||||||
|
|
||||||
|
expect(location.reloadPage).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when destroyed', () => {
|
describe('when destroyed', () => {
|
||||||
|
@ -201,6 +218,17 @@ describe('SwUpdatesService', () => {
|
||||||
swu.$$activatedSubj.next({current: {hash: 'qux'}});
|
swu.$$activatedSubj.next({current: {hash: 'qux'}});
|
||||||
expect(location.fullPageNavigationNeeded).not.toHaveBeenCalled();
|
expect(location.fullPageNavigationNeeded).not.toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should stop requesting page reloads when unrecoverable states are detected', run(() => {
|
||||||
|
swu.$$unrecoverableSubj.next({reason: 'Something bad happened'});
|
||||||
|
expect(location.reloadPage).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
service.ngOnDestroy();
|
||||||
|
location.reloadPage.calls.reset();
|
||||||
|
|
||||||
|
swu.$$unrecoverableSubj.next({reason: 'Something worse happened'});
|
||||||
|
expect(location.reloadPage).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -212,9 +240,11 @@ class MockApplicationRef {
|
||||||
class MockSwUpdate {
|
class MockSwUpdate {
|
||||||
$$availableSubj = new Subject<{available: {hash: string}}>();
|
$$availableSubj = new Subject<{available: {hash: string}}>();
|
||||||
$$activatedSubj = new Subject<{current: {hash: string}}>();
|
$$activatedSubj = new Subject<{current: {hash: string}}>();
|
||||||
|
$$unrecoverableSubj = new Subject<{reason: string}>();
|
||||||
|
|
||||||
available = this.$$availableSubj.asObservable();
|
available = this.$$availableSubj.asObservable();
|
||||||
activated = this.$$activatedSubj.asObservable();
|
activated = this.$$activatedSubj.asObservable();
|
||||||
|
unrecoverable = this.$$unrecoverableSubj.asObservable();
|
||||||
|
|
||||||
activateUpdate = jasmine.createSpy('MockSwUpdate.activateUpdate')
|
activateUpdate = jasmine.createSpy('MockSwUpdate.activateUpdate')
|
||||||
.and.callFake(() => Promise.resolve());
|
.and.callFake(() => Promise.resolve());
|
||||||
|
|
|
@ -51,6 +51,14 @@ export class SwUpdatesService implements OnDestroy {
|
||||||
takeUntil(this.onDestroy),
|
takeUntil(this.onDestroy),
|
||||||
)
|
)
|
||||||
.subscribe(() => location.fullPageNavigationNeeded());
|
.subscribe(() => location.fullPageNavigationNeeded());
|
||||||
|
|
||||||
|
// Request an immediate page reload once an unrecoverable state has been detected.
|
||||||
|
this.swu.unrecoverable
|
||||||
|
.pipe(
|
||||||
|
tap(evt => this.log(`Unrecoverable state: ${evt.reason}\nReloading...`)),
|
||||||
|
takeUntil(this.onDestroy),
|
||||||
|
)
|
||||||
|
.subscribe(() => location.reloadPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
|
@ -13,6 +13,7 @@ export class MockLocationService {
|
||||||
.callFake((url: string) => this.urlSubject.next(url));
|
.callFake((url: string) => this.urlSubject.next(url));
|
||||||
goExternal = jasmine.createSpy('Location.goExternal');
|
goExternal = jasmine.createSpy('Location.goExternal');
|
||||||
replace = jasmine.createSpy('Location.replace');
|
replace = jasmine.createSpy('Location.replace');
|
||||||
|
reloadPage = jasmine.createSpy('Location.reloadPage');
|
||||||
handleAnchorClick = jasmine.createSpy('Location.handleAnchorClick')
|
handleAnchorClick = jasmine.createSpy('Location.handleAnchorClick')
|
||||||
.and.returnValue(false); // prevent click from causing a browser navigation
|
.and.returnValue(false); // prevent click from causing a browser navigation
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3033,
|
"runtime-es2015": 3033,
|
||||||
"main-es2015": 447565,
|
"main-es2015": 447766,
|
||||||
"polyfills-es2015": 52343
|
"polyfills-es2015": 52343
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3033,
|
"runtime-es2015": 3033,
|
||||||
"main-es2015": 447774,
|
"main-es2015": 447975,
|
||||||
"polyfills-es2015": 52493
|
"polyfills-es2015": 52493
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue