From 630028350a085a4e9a115bdd31ac52e5b668f9e9 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 2 Aug 2016 07:38:14 -0700 Subject: [PATCH] refactor(core): Introduce `AppInitStatus` This class allows any provider to know and wait for the initialization of the application. This functionality previously was tied to `ApplicationRef`. BREAKING CHANGE: - `ApplicationRef.waitForAsyncInitializers` is deprecated. Use `AppInitStatus.donePromise` / `AppInitStatus.done` instead. --- modules/@angular/core/index.ts | 3 +- modules/@angular/core/src/application_init.ts | 49 ++++++++++++++++++ .../@angular/core/src/application_module.ts | 2 + modules/@angular/core/src/application_ref.ts | 34 +++++-------- .../@angular/core/src/application_tokens.ts | 6 --- .../core/test/application_init_spec.ts | 50 +++++++++++++++++++ .../core/test/application_ref_spec.ts | 34 +++++++++++-- 7 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 modules/@angular/core/src/application_init.ts create mode 100644 modules/@angular/core/test/application_init_spec.ts diff --git a/modules/@angular/core/index.ts b/modules/@angular/core/index.ts index 555f067afb..301cfbbea6 100644 --- a/modules/@angular/core/index.ts +++ b/modules/@angular/core/index.ts @@ -15,7 +15,8 @@ export * from './src/metadata'; export * from './src/util'; export * from './src/di'; export {createPlatform, assertPlatform, disposePlatform, getPlatform, coreBootstrap, coreLoadAndBootstrap, PlatformRef, ApplicationRef, enableProdMode, lockRunMode, isDevMode, createPlatformFactory} from './src/application_ref'; -export {APP_ID, APP_INITIALIZER, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, APP_BOOTSTRAP_LISTENER} from './src/application_tokens'; +export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, APP_BOOTSTRAP_LISTENER} from './src/application_tokens'; +export {APP_INITIALIZER, AppInitStatus} from './src/application_init'; export * from './src/zone'; export * from './src/render'; export * from './src/linker'; diff --git a/modules/@angular/core/src/application_init.ts b/modules/@angular/core/src/application_init.ts new file mode 100644 index 0000000000..547017c604 --- /dev/null +++ b/modules/@angular/core/src/application_init.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {isPromise} from '../src/facade/lang'; + +import {Inject, Injectable, OpaqueToken, Optional} from './di'; + + +/** + * A function that will be executed when an application is initialized. + * @experimental + */ +export const APP_INITIALIZER: any = new OpaqueToken('Application Initializer'); + +/** + * A class that reflects the state of running {@link APP_INITIALIZER}s. + * + * @experimental + */ +@Injectable() +export class AppInitStatus { + private _donePromise: Promise; + private _done = false; + + constructor(@Inject(APP_INITIALIZER) @Optional() appInits: (() => any)[]) { + const asyncInitPromises: Promise[] = []; + if (appInits) { + for (let i = 0; i < appInits.length; i++) { + const initResult = appInits[i](); + if (isPromise(initResult)) { + asyncInitPromises.push(initResult); + } + } + } + this._donePromise = Promise.all(asyncInitPromises).then(() => { this._done = true; }); + if (asyncInitPromises.length === 0) { + this._done = true; + } + } + + get done(): boolean { return this._done; } + + get donePromise(): Promise { return this._donePromise; } +} diff --git a/modules/@angular/core/src/application_module.ts b/modules/@angular/core/src/application_module.ts index e75befa4fe..b102c1c2e4 100644 --- a/modules/@angular/core/src/application_module.ts +++ b/modules/@angular/core/src/application_module.ts @@ -8,6 +8,7 @@ import {Type} from '../src/facade/lang'; +import {AppInitStatus} from './application_init'; import {ApplicationRef, ApplicationRef_, isDevMode} from './application_ref'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; @@ -44,6 +45,7 @@ export const APPLICATION_COMMON_PROVIDERS: Array providers: [ ApplicationRef_, {provide: ApplicationRef, useExisting: ApplicationRef_}, + AppInitStatus, Compiler, {provide: ComponentResolver, useExisting: Compiler}, APP_ID_RANDOM_PROVIDER, diff --git a/modules/@angular/core/src/application_ref.ts b/modules/@angular/core/src/application_ref.ts index 01b5432a38..9f9dd23666 100644 --- a/modules/@angular/core/src/application_ref.ts +++ b/modules/@angular/core/src/application_ref.ts @@ -11,7 +11,8 @@ import {ListWrapper} from '../src/facade/collection'; import {BaseException, ExceptionHandler, unimplemented} from '../src/facade/exceptions'; import {ConcreteType, Type, isBlank, isPresent, isPromise, stringify} from '../src/facade/lang'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, PLATFORM_INITIALIZER} from './application_tokens'; +import {AppInitStatus} from './application_init'; +import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens'; import {ChangeDetectorRef} from './change_detection/change_detector_ref'; import {Console} from './console'; import {Inject, Injectable, Injector, OpaqueToken, Optional, ReflectiveInjector, SkipSelf, forwardRef} from './di'; @@ -359,19 +360,8 @@ export class PlatformRef_ extends PlatformRef { exceptionHandler.call(error.error, error.stackTrace); }); return _callAndReportToExceptionHandler(exceptionHandler, () => { - const appInits = moduleRef.injector.get(APP_INITIALIZER, null); - const asyncInitPromises: Promise[] = []; - if (isPresent(appInits)) { - for (let i = 0; i < appInits.length; i++) { - const initResult = appInits[i](); - if (isPromise(initResult)) { - asyncInitPromises.push(initResult); - } - } - } - const appRef: ApplicationRef_ = moduleRef.injector.get(ApplicationRef); - return Promise.all(asyncInitPromises).then(() => { - appRef.asyncInitDone(); + const initStatus: AppInitStatus = moduleRef.injector.get(AppInitStatus); + return initStatus.donePromise.then(() => { this._moduleDoBootstrap(moduleRef); return moduleRef; }); @@ -430,6 +420,8 @@ export abstract class ApplicationRef { /** * Returns a promise that resolves when all asynchronous application initializers * are done. + * + * @deprecated Use the {@link AppInitStatus} class instead. */ abstract waitForAsyncInitializers(): Promise; @@ -509,13 +501,11 @@ export class ApplicationRef_ extends ApplicationRef { private _runningTick: boolean = false; private _enforceNoNewChanges: boolean = false; - /** @internal */ - _asyncInitDonePromise: PromiseCompleter = PromiseWrapper.completer(); - constructor( private _zone: NgZone, private _console: Console, private _injector: Injector, private _exceptionHandler: ExceptionHandler, private _componentFactoryResolver: ComponentFactoryResolver, + private _initStatus: AppInitStatus, @Optional() private _testabilityRegistry: TestabilityRegistry, @Optional() private _testability: Testability) { super(); @@ -545,11 +535,9 @@ export class ApplicationRef_ extends ApplicationRef { } /** - * @internal + * @deprecated */ - asyncInitDone() { this._asyncInitDonePromise.resolve(null); } - - waitForAsyncInitializers(): Promise { return this._asyncInitDonePromise.promise; } + waitForAsyncInitializers(): Promise { return this._initStatus.donePromise; } run(callback: Function): any { return this._zone.run( @@ -557,6 +545,10 @@ export class ApplicationRef_ extends ApplicationRef { } bootstrap(componentOrFactory: ComponentFactory|ConcreteType): ComponentRef { + if (!this._initStatus.done) { + throw new BaseException( + 'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.'); + } return this.run(() => { let componentFactory: ComponentFactory; if (componentOrFactory instanceof ComponentFactory) { diff --git a/modules/@angular/core/src/application_tokens.ts b/modules/@angular/core/src/application_tokens.ts index a5f5cb00b5..9992d81818 100644 --- a/modules/@angular/core/src/application_tokens.ts +++ b/modules/@angular/core/src/application_tokens.ts @@ -47,12 +47,6 @@ function _randomChar(): string { */ export const PLATFORM_INITIALIZER: any = new OpaqueToken('Platform Initializer'); -/** - * A function that will be executed when an application is initialized. - * @experimental - */ -export const APP_INITIALIZER: any = new OpaqueToken('Application Initializer'); - /** * All callbacks provided via this token will be called when a component has been bootstrapped. * diff --git a/modules/@angular/core/test/application_init_spec.ts b/modules/@angular/core/test/application_init_spec.ts new file mode 100644 index 0000000000..c86a768476 --- /dev/null +++ b/modules/@angular/core/test/application_init_spec.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {APP_INITIALIZER, AppInitStatus} from '../src/application_init'; +import {PromiseCompleter, PromiseWrapper} from '../src/facade/async'; +import {TestBed, async, inject, withModule} from '../testing'; + +export function main() { + describe('AppInitStatus', () => { + describe('no initializers', () => { + + it('should return true for `done`', + inject([AppInitStatus], (status: AppInitStatus) => { expect(status.done).toBe(true); })); + + it('should return a promise that resolves immediately for `donePromise`', + async(inject([AppInitStatus], (status: AppInitStatus) => { + status.donePromise.then(() => { expect(status.done).toBe(true); }); + }))); + }); + + describe('with async initializers', () => { + let completer: PromiseCompleter; + beforeEach(() => { + completer = PromiseWrapper.completer(); + TestBed.configureTestingModule({ + providers: [{provide: APP_INITIALIZER, multi: true, useValue: () => completer.promise}] + }); + }); + + it('should updat the status once all async initializers are done', + async(inject([AppInitStatus], (status: AppInitStatus) => { + let completerResolver = false; + setTimeout(() => { + completerResolver = true; + completer.resolve(); + }); + + expect(status.done).toBe(false); + status.donePromise.then(() => { + expect(status.done).toBe(true); + expect(completerResolver).toBe(true); + }); + }))); + }); + }); +} \ No newline at end of file diff --git a/modules/@angular/core/test/application_ref_spec.ts b/modules/@angular/core/test/application_ref_spec.ts index cb6cb1dd13..6b888bc043 100644 --- a/modules/@angular/core/test/application_ref_spec.ts +++ b/modules/@angular/core/test/application_ref_spec.ts @@ -19,7 +19,7 @@ import {PromiseCompleter, PromiseWrapper} from '../src/facade/async'; import {ExceptionHandler} from '../src/facade/exception_handler'; import {BaseException} from '../src/facade/exceptions'; import {ConcreteType} from '../src/facade/lang'; -import {TestBed, async, inject} from '../testing'; +import {TestBed, async, inject, withModule} from '../testing'; import {SpyChangeDetectorRef} from './spies'; @@ -133,7 +133,27 @@ export function main() { const compRef = ref.bootstrap(SomeComponent); expect(capturedCompRefs).toEqual([compRef]); })); + }); + describe('bootstrap', () => { + beforeEach( + () => { + + }); + it('should throw if an APP_INITIIALIZER is not yet resolved', + withModule( + { + providers: [{ + provide: APP_INITIALIZER, + useValue: () => PromiseWrapper.completer().promise, + multi: true + }] + }, + inject([ApplicationRef], (ref: ApplicationRef_) => { + expect(() => ref.bootstrap(SomeComponent)) + .toThrowError( + 'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.'); + }))); }); }); @@ -163,7 +183,11 @@ export function main() { [{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}])) .then(() => expect(false).toBe(true), (e) => { expect(e).toBe('Test'); - expect(errorLogger.res).toEqual(['EXCEPTION: Test']); + // Note: if the modules throws an error during construction, + // we don't have an injector and therefore no way of + // getting the exception handler. So + // the error is only rethrown but not logged via the exception handler. + expect(errorLogger.res).toEqual([]); }); })); @@ -248,7 +272,11 @@ export function main() { const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule( [{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}])); expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test'); - expect(errorLogger.res).toEqual(['EXCEPTION: Test']); + // Note: if the modules throws an error during construction, + // we don't have an injector and therefore no way of + // getting the exception handler. So + // the error is only rethrown but not logged via the exception handler. + expect(errorLogger.res).toEqual([]); })); it('should rethrow promise errors even if the exceptionHandler is not rethrowing',