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();