feat(core): support APP_INITIALIZER work with observable (#33222)

This commit adds support for Observables that now can be used as a part of APP_INITIALIZER. Previously, only
Primises were supported.

Closes #15088.

PR Close #33222
This commit is contained in:
vthinkxie 2019-10-17 17:48:35 +08:00 committed by atscott
parent 53c65f468f
commit ca17ac523c
4 changed files with 137 additions and 24 deletions

View File

@ -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)) {
const observableAsPromise = new Promise<void>((resolve, reject) => {
initResult.subscribe({complete: resolve, error: reject});
});
asyncInitPromises.push(observableAsPromise);
}
}
}

View File

@ -5,8 +5,8 @@
* 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 {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,50 +28,152 @@ 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<any>;
let completerResolver = false;
let initFnInvoked = false;
beforeEach(() => {
let initializerFactory = (injector: Injector) => {
return () => {
const initStatus = injector.get(ApplicationInitStatus);
initStatus.donePromise.then(() => {
expect(completerResolver).toBe(true);
});
};
};
promise = new Promise((res) => {
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
TestBed.configureTestingModule({
providers: [
{provide: APP_INITIALIZER, multi: true, useValue: () => promise},
{
provide: APP_INITIALIZER,
multi: true,
useFactory: initializerFactory,
deps: [Injector]
},
]
});
});
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<any>;
let observable: Observable<any>;
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 completed',
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.complete();
});
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
})));
it('should update the status once all async observable initializers nexted and completed',
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();
subscriber.next('one');
subscriber.next('two');
setTimeout(() => {
initFnInvoked = true;
subscriber.complete();
});
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).toBe(true);
expect(initFnInvoked).toBe(true);
});
})));
it('should update the status if all async observable initializers are completed before runInitializers',
waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => {
subscriber.complete();
// Accessing internal `runInitializers` function of the `ApplicationInitStatus` class
// instance for testing purposes to invoke initializer functions.
(status as any).runInitializers();
expect(status.done).toBe(false);
status.donePromise.then(() => {
expect(status.done).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);
});
})));
});
});
}

View File

@ -1253,6 +1253,9 @@
{
"name": "isObject"
},
{
"name": "isObservable"
},
{
"name": "isOptionsObj"
},

View File

@ -1580,6 +1580,9 @@
{
"name": "isObject"
},
{
"name": "isObservable"
},
{
"name": "isPositive"
},