From 00efacf561e8e935dcfbf48bab153aab3247df9d Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Tue, 24 Mar 2020 22:54:51 +0200 Subject: [PATCH] feat(service-worker): support timeout in `registerWhenStable` SW registration strategy (#35870) Previously, when using the `registerWhenStable` ServiceWorker registration strategy (which is also the default) Angular would wait indefinitely for the [app to stabilize][1], before registering the ServiceWorker script. This could lead to a situation where the ServiceWorker would never be registered when there was a long-running task (such as an interval or recurring timeout). Such tasks can often be started by a 3rd-party dependency (beyond the developer's control or even without them realizing). In addition, this situation is particularly hard to detect, because the ServiceWorker is typically not used during development and on production builds a previous ServiceWorker instance might be already active. This commit enhances the `registerWhenStable` registration strategy by adding support for an optional `` argument, which guarantees that the ServiceWorker will be registered when the timeout expires, even if the app has not stabilized yet. For example, with `registerWhenStable:5000` the ServiceWorker will be registered as soon as the app stabilizes or after 5 seconds if the app has not stabilized by then. Related to #34464. [1]: https://angular.io/api/core/ApplicationRef#is-stable-examples PR Close #35870 --- packages/service-worker/src/module.ts | 25 +++++-- packages/service-worker/test/module_spec.ts | 74 ++++++++++++++++++++- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/packages/service-worker/src/module.ts b/packages/service-worker/src/module.ts index 4979f2d692..7e7779f5d7 100644 --- a/packages/service-worker/src/module.ts +++ b/packages/service-worker/src/module.ts @@ -8,7 +8,7 @@ import {isPlatformBrowser} from '@angular/common'; import {APP_INITIALIZER, ApplicationRef, InjectionToken, Injector, ModuleWithProviders, NgModule, PLATFORM_ID} from '@angular/core'; -import {Observable, of } from 'rxjs'; +import {Observable, merge, of } from 'rxjs'; import {delay, filter, take} from 'rxjs/operators'; import {NgswCommChannel} from './low_level'; @@ -55,8 +55,12 @@ export abstract class SwRegistrationOptions { * registered (e.g. there might be a long-running timeout or polling interval, preventing the app * to stabilize). The available option are: * - * - `registerWhenStable`: Register as soon as the application stabilizes (no pending - * micro-/macro-tasks). + * - `registerWhenStable:`: Register as soon as the application stabilizes (no pending + * micro-/macro-tasks) but no later than `` milliseconds. If the app hasn't + * stabilized after `` milliseconds (for example, due to a recurrent asynchronous + * task), the ServiceWorker will be registered anyway. + * If `` is omitted, the ServiceWorker will only be registered once the app + * stabilizes. * - `registerImmediately`: Register immediately. * - `registerWithDelay:`: Register with a delay of `` milliseconds. For * example, use `registerWithDelay:5000` to register the ServiceWorker after 5 seconds. If @@ -102,11 +106,11 @@ export function ngswAppInitializer( readyToRegister$ = of (null); break; case 'registerWithDelay': - readyToRegister$ = of (null).pipe(delay(+args[0] || 0)); + readyToRegister$ = delayWithTimeout(+args[0] || 0); break; case 'registerWhenStable': - const appRef = injector.get(ApplicationRef); - readyToRegister$ = appRef.isStable.pipe(filter(stable => stable)); + readyToRegister$ = !args[0] ? whenStable(injector) : + merge(whenStable(injector), delayWithTimeout(+args[0])); break; default: // Unknown strategy. @@ -124,6 +128,15 @@ export function ngswAppInitializer( return initializer; } +function delayWithTimeout(timeout: number): Observable { + return of (null).pipe(delay(timeout)); +} + +function whenStable(injector: Injector): Observable { + const appRef = injector.get(ApplicationRef); + return appRef.isStable.pipe(filter(stable => stable)); +} + export function ngswCommChannelFactory( opts: SwRegistrationOptions, platformId: string): NgswCommChannel { return new NgswCommChannel( diff --git a/packages/service-worker/test/module_spec.ts b/packages/service-worker/test/module_spec.ts index 0276d0b875..46591e100f 100644 --- a/packages/service-worker/test/module_spec.ts +++ b/packages/service-worker/test/module_spec.ts @@ -141,7 +141,7 @@ describe('ServiceWorkerModule', () => { ], }); - // Dummy `get()` call to initialize the test "app". + // Dummy `inject()` call to initialize the test "app". TestBed.inject(ApplicationRef); return isStableSub; @@ -156,13 +156,80 @@ describe('ServiceWorkerModule', () => { tick(); expect(swRegisterSpy).not.toHaveBeenCalled(); + tick(60000); + expect(swRegisterSpy).not.toHaveBeenCalled(); + isStableSub.next(true); tick(); expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); })); - it('registers the SW when the app stabilizes with `registerWhenStable`', fakeAsync(() => { + it('registers the SW when the app stabilizes with `registerWhenStable:`', + fakeAsync(() => { + const isStableSub = configTestBedWithMockedStability('registerWhenStable:1000'); + + isStableSub.next(false); + isStableSub.next(false); + + tick(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(500); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + isStableSub.next(true); + + tick(); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW after `timeout` if the app does not stabilize with `registerWhenStable:`', + fakeAsync(() => { + configTestBedWithMockedStability('registerWhenStable:1000'); + + tick(999); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(1); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW asap (asynchronously) before the app stabilizes with `registerWhenStable:0`', + fakeAsync(() => { + const isStableSub = configTestBedWithMockedStability('registerWhenStable:0'); + + // Create a microtask. + Promise.resolve(); + + flushMicrotasks(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(0); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW only when the app stabilizes with `registerWhenStable:`', + fakeAsync(() => { + const isStableSub = configTestBedWithMockedStability('registerWhenStable:'); + + isStableSub.next(false); + isStableSub.next(false); + + tick(); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + tick(60000); + expect(swRegisterSpy).not.toHaveBeenCalled(); + + isStableSub.next(true); + + tick(); + expect(swRegisterSpy).toHaveBeenCalledWith('sw.js', {scope: undefined}); + })); + + it('registers the SW only when the app stabilizes with `registerWhenStable`', + fakeAsync(() => { const isStableSub = configTestBedWithMockedStability('registerWhenStable'); isStableSub.next(false); @@ -171,6 +238,9 @@ describe('ServiceWorkerModule', () => { tick(); expect(swRegisterSpy).not.toHaveBeenCalled(); + tick(60000); + expect(swRegisterSpy).not.toHaveBeenCalled(); + isStableSub.next(true); tick();