diff --git a/packages/core/src/application_init.ts b/packages/core/src/application_init.ts index d464573b44b0b..06f3b74883bea 100644 --- a/packages/core/src/application_init.ts +++ b/packages/core/src/application_init.ts @@ -7,7 +7,7 @@ */ import {Inject, Injectable, InjectionToken, Optional} from './di'; -import {isPromise} from './util/lang'; +import {isObservable, isPromise} from './util/lang'; import {noop} from './util/noop'; @@ -16,8 +16,8 @@ import {noop} from './util/noop'; * one or more initialization functions. * * The provided functions are injected at application startup and executed during - * app initialization. If any of these functions returns a Promise, initialization - * does not complete until the Promise is resolved. + * app initialization. If any of these functions returns a Promise or an Observable, initialization + * does not complete until the Promise is resolved or the Observable is completed. * * You can, for example, create a factory function that loads language data * or an external configuration, and provide that function to the `APP_INITIALIZER` token. @@ -68,6 +68,11 @@ export class ApplicationInitStatus { const initResult = this.appInits[i](); if (isPromise(initResult)) { asyncInitPromises.push(initResult); + } else if (isObservable(initResult)) { + // Note: `.toPromise()` will be deprecated in RxJS v7 and removed completely in later + // versions. Once RxJS is updated in the repo to v7, this code should be refactored and + // the `lastValueFrom` function should be used instead. + asyncInitPromises.push(initResult.toPromise()); } } } diff --git a/packages/core/test/application_init_spec.ts b/packages/core/test/application_init_spec.ts index ee078a606ea0f..0df7d39b11175 100644 --- a/packages/core/test/application_init_spec.ts +++ b/packages/core/test/application_init_spec.ts @@ -7,6 +7,7 @@ */ import {Injector} from '@angular/core'; import {APP_INITIALIZER, ApplicationInitStatus} from '@angular/core/src/application_init'; +import {Observable, Subscriber} from 'rxjs'; import {inject, TestBed, waitForAsync} from '../testing'; @@ -28,21 +29,23 @@ import {inject, TestBed, waitForAsync} from '../testing'; }))); }); - describe('with async initializers', () => { + describe('with async promise initializers', () => { let resolve: (result: any) => void; + let reject: (reason?: any) => void; let promise: Promise; - let completerResolver = false; + let initFnInvoked = false; beforeEach(() => { let initializerFactory = (injector: Injector) => { return () => { const initStatus = injector.get(ApplicationInitStatus); initStatus.donePromise.then(() => { - expect(completerResolver).toBe(true); + expect(initFnInvoked).toBe(true); }); }; }; - promise = new Promise((res) => { + promise = new Promise((res, rej) => { resolve = res; + reject = rej; }); TestBed.configureTestingModule({ providers: [ @@ -57,21 +60,101 @@ import {inject, TestBed, waitForAsync} from '../testing'; }); }); - it('should update the status once all async initializers are done', + it('should update the status once all async promise initializers are done', waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { + // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class + // instance for testing purposes to invoke initializer functions. (status as any).runInitializers(); setTimeout(() => { - completerResolver = true; + initFnInvoked = true; resolve(null); }); expect(status.done).toBe(false); status.donePromise.then(() => { expect(status.done).toBe(true); - expect(completerResolver).toBe(true); + expect(initFnInvoked).toBe(true); }); }))); + + it('should handle a case when promise is rejected', + waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { + // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class + // instance for testing purposes to invoke initializer functions. + (status as any).runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + reject(); + }); + + expect(status.done).toBe(false); + status.donePromise + .then( + () => fail('`donePromise.then` should not be invoked when promise is rejected')) + .catch(() => { + expect(status.done).toBe(false); + expect(initFnInvoked).toBe(true); + }); + }))); + }); + + describe('with app initializers represented using observables', () => { + let subscriber: Subscriber; + let observable: Observable; + let initFnInvoked = false; + beforeEach(() => { + observable = new Observable((res) => { + subscriber = res; + }); + TestBed.configureTestingModule({ + providers: [ + {provide: APP_INITIALIZER, multi: true, useValue: () => observable}, + ] + }); + }); + + it('should update the status once all async observable initializers are done', + waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { + // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class + // instance for testing purposes to invoke initializer functions. + (status as any).runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + subscriber.next(); + subscriber.complete(); + }); + + expect(status.done).toBe(false); + status.donePromise.then(() => { + expect(status.done).toBe(true); + expect(initFnInvoked).toBe(true); + }); + }))); + + it('should handle a case when observable emits an error', + waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { + // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class + // instance for testing purposes to invoke initializer functions. + (status as any).runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + subscriber.error(); + }); + + expect(status.done).toBe(false); + status.donePromise + .then( + () => fail( + '`donePromise.then` should not be invoked when observable emits an error')) + .catch(() => { + expect(status.done).toBe(false); + expect(initFnInvoked).toBe(true); + }); + }))); }); }); } diff --git a/packages/core/test/bundling/forms/bundle.golden_symbols.json b/packages/core/test/bundling/forms/bundle.golden_symbols.json index 8ef0dcdff914b..b3f2e76e2eb2a 100644 --- a/packages/core/test/bundling/forms/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms/bundle.golden_symbols.json @@ -1250,6 +1250,9 @@ { "name": "isObject" }, + { + "name": "isObservable" + }, { "name": "isOptionsObj" }, diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index d7ca828b94523..2662be9614d27 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1577,6 +1577,9 @@ { "name": "isObject" }, + { + "name": "isObservable" + }, { "name": "isPositive" },