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.
This commit is contained in:
Tobias Bosch 2016-08-02 07:38:14 -07:00
parent 7e4fd7d7da
commit 630028350a
7 changed files with 147 additions and 31 deletions

View File

@ -15,7 +15,8 @@ export * from './src/metadata';
export * from './src/util'; export * from './src/util';
export * from './src/di'; export * from './src/di';
export {createPlatform, assertPlatform, disposePlatform, getPlatform, coreBootstrap, coreLoadAndBootstrap, PlatformRef, ApplicationRef, enableProdMode, lockRunMode, isDevMode, createPlatformFactory} from './src/application_ref'; 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/zone';
export * from './src/render'; export * from './src/render';
export * from './src/linker'; export * from './src/linker';

View File

@ -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<any>;
private _done = false;
constructor(@Inject(APP_INITIALIZER) @Optional() appInits: (() => any)[]) {
const asyncInitPromises: Promise<any>[] = [];
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<any> { return this._donePromise; }
}

View File

@ -8,6 +8,7 @@
import {Type} from '../src/facade/lang'; import {Type} from '../src/facade/lang';
import {AppInitStatus} from './application_init';
import {ApplicationRef, ApplicationRef_, isDevMode} from './application_ref'; import {ApplicationRef, ApplicationRef_, isDevMode} from './application_ref';
import {APP_ID_RANDOM_PROVIDER} from './application_tokens'; import {APP_ID_RANDOM_PROVIDER} from './application_tokens';
import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection'; import {IterableDiffers, KeyValueDiffers, defaultIterableDiffers, defaultKeyValueDiffers} from './change_detection/change_detection';
@ -44,6 +45,7 @@ export const APPLICATION_COMMON_PROVIDERS: Array<Type|{[k: string]: any}|any[]>
providers: [ providers: [
ApplicationRef_, ApplicationRef_,
{provide: ApplicationRef, useExisting: ApplicationRef_}, {provide: ApplicationRef, useExisting: ApplicationRef_},
AppInitStatus,
Compiler, Compiler,
{provide: ComponentResolver, useExisting: Compiler}, {provide: ComponentResolver, useExisting: Compiler},
APP_ID_RANDOM_PROVIDER, APP_ID_RANDOM_PROVIDER,

View File

@ -11,7 +11,8 @@ import {ListWrapper} from '../src/facade/collection';
import {BaseException, ExceptionHandler, unimplemented} from '../src/facade/exceptions'; import {BaseException, ExceptionHandler, unimplemented} from '../src/facade/exceptions';
import {ConcreteType, Type, isBlank, isPresent, isPromise, stringify} from '../src/facade/lang'; 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 {ChangeDetectorRef} from './change_detection/change_detector_ref';
import {Console} from './console'; import {Console} from './console';
import {Inject, Injectable, Injector, OpaqueToken, Optional, ReflectiveInjector, SkipSelf, forwardRef} from './di'; 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); exceptionHandler.call(error.error, error.stackTrace);
}); });
return _callAndReportToExceptionHandler(exceptionHandler, () => { return _callAndReportToExceptionHandler(exceptionHandler, () => {
const appInits = moduleRef.injector.get(APP_INITIALIZER, null); const initStatus: AppInitStatus = moduleRef.injector.get(AppInitStatus);
const asyncInitPromises: Promise<any>[] = []; return initStatus.donePromise.then(() => {
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();
this._moduleDoBootstrap(moduleRef); this._moduleDoBootstrap(moduleRef);
return moduleRef; return moduleRef;
}); });
@ -430,6 +420,8 @@ export abstract class ApplicationRef {
/** /**
* Returns a promise that resolves when all asynchronous application initializers * Returns a promise that resolves when all asynchronous application initializers
* are done. * are done.
*
* @deprecated Use the {@link AppInitStatus} class instead.
*/ */
abstract waitForAsyncInitializers(): Promise<any>; abstract waitForAsyncInitializers(): Promise<any>;
@ -509,13 +501,11 @@ export class ApplicationRef_ extends ApplicationRef {
private _runningTick: boolean = false; private _runningTick: boolean = false;
private _enforceNoNewChanges: boolean = false; private _enforceNoNewChanges: boolean = false;
/** @internal */
_asyncInitDonePromise: PromiseCompleter<any> = PromiseWrapper.completer();
constructor( constructor(
private _zone: NgZone, private _console: Console, private _injector: Injector, private _zone: NgZone, private _console: Console, private _injector: Injector,
private _exceptionHandler: ExceptionHandler, private _exceptionHandler: ExceptionHandler,
private _componentFactoryResolver: ComponentFactoryResolver, private _componentFactoryResolver: ComponentFactoryResolver,
private _initStatus: AppInitStatus,
@Optional() private _testabilityRegistry: TestabilityRegistry, @Optional() private _testabilityRegistry: TestabilityRegistry,
@Optional() private _testability: Testability) { @Optional() private _testability: Testability) {
super(); super();
@ -545,11 +535,9 @@ export class ApplicationRef_ extends ApplicationRef {
} }
/** /**
* @internal * @deprecated
*/ */
asyncInitDone() { this._asyncInitDonePromise.resolve(null); } waitForAsyncInitializers(): Promise<any> { return this._initStatus.donePromise; }
waitForAsyncInitializers(): Promise<any> { return this._asyncInitDonePromise.promise; }
run(callback: Function): any { run(callback: Function): any {
return this._zone.run( return this._zone.run(
@ -557,6 +545,10 @@ export class ApplicationRef_ extends ApplicationRef {
} }
bootstrap<C>(componentOrFactory: ComponentFactory<C>|ConcreteType<C>): ComponentRef<C> { bootstrap<C>(componentOrFactory: ComponentFactory<C>|ConcreteType<C>): ComponentRef<C> {
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(() => { return this.run(() => {
let componentFactory: ComponentFactory<C>; let componentFactory: ComponentFactory<C>;
if (componentOrFactory instanceof ComponentFactory) { if (componentOrFactory instanceof ComponentFactory) {

View File

@ -47,12 +47,6 @@ function _randomChar(): string {
*/ */
export const PLATFORM_INITIALIZER: any = new OpaqueToken('Platform Initializer'); 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. * All callbacks provided via this token will be called when a component has been bootstrapped.
* *

View File

@ -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<any>;
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);
});
})));
});
});
}

View File

@ -19,7 +19,7 @@ import {PromiseCompleter, PromiseWrapper} from '../src/facade/async';
import {ExceptionHandler} from '../src/facade/exception_handler'; import {ExceptionHandler} from '../src/facade/exception_handler';
import {BaseException} from '../src/facade/exceptions'; import {BaseException} from '../src/facade/exceptions';
import {ConcreteType} from '../src/facade/lang'; import {ConcreteType} from '../src/facade/lang';
import {TestBed, async, inject} from '../testing'; import {TestBed, async, inject, withModule} from '../testing';
import {SpyChangeDetectorRef} from './spies'; import {SpyChangeDetectorRef} from './spies';
@ -133,7 +133,27 @@ export function main() {
const compRef = ref.bootstrap(SomeComponent); const compRef = ref.bootstrap(SomeComponent);
expect(capturedCompRefs).toEqual([compRef]); 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}])) [{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
.then(() => expect(false).toBe(true), (e) => { .then(() => expect(false).toBe(true), (e) => {
expect(e).toBe('Test'); 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( const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}])); [{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test'); 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', it('should rethrow promise errors even if the exceptionHandler is not rethrowing',