Skip to content

Commit 30f9e48

Browse files
Julien HellerNoNameProvided
authored andcommitted
feat: add functionality to async initialize services
1 parent 4e2ceb1 commit 30f9e48

File tree

7 files changed

+277
-0
lines changed

7 files changed

+277
-0
lines changed

src/container-instance.class.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { Token } from './token.class';
55
import { Constructable } from './types/constructable.type';
66
import { ServiceIdentifier } from './types/service-identifier.type';
77
import { ServiceMetadata } from './interfaces/service-metadata.interface.';
8+
import { AsyncInitializedService } from './types/AsyncInitializedService';
9+
import { MissingInitializedPromiseError } from './error/MissingInitializedPromiseError';
10+
811

912
/**
1013
* TypeDI can have multiple containers.
@@ -115,6 +118,74 @@ export class ContainerInstance {
115118
return this.getServiceValue(identifier, service);
116119
}
117120

121+
/**
122+
* Like get, but returns a promise of a service that recursively resolves async properties.
123+
* Used when service defined with asyncInitialization: true flag.
124+
*/
125+
getAsync<T>(type: ObjectType<T>): Promise<T>;
126+
127+
/**
128+
* Like get, but returns a promise of a service that recursively resolves async properties.
129+
* Used when service defined with asyncInitialization: true flag.
130+
*/
131+
getAsync<T>(id: string): Promise<T>;
132+
133+
/**
134+
* Like get, but returns a promise of a service that recursively resolves async properties.
135+
* Used when service defined with asyncInitialization: true flag.
136+
*/
137+
getAsync<T>(id: Token<T>): Promise<T>;
138+
139+
/**
140+
* Like get, but returns a promise of a service that recursively resolves async properties.
141+
* Used when service defined with asyncInitialization: true flag.
142+
*/
143+
getAsync<T>(id: { service: T }): Promise<T>;
144+
145+
/**
146+
* Like get, but returns a promise of a service that recursively resolves async properties.
147+
* Used when service defined with asyncInitialization: true flag.
148+
*/
149+
getAsync<T>(identifier: ServiceIdentifier<T>): Promise<T> {
150+
const globalContainer = Container.of(undefined);
151+
const service = globalContainer.findService(identifier);
152+
const scopedService = this.findService(identifier);
153+
154+
if (service && service.global === true) return this.getServiceValueAsync(identifier, service);
155+
156+
if (scopedService) return this.getServiceValueAsync(identifier, scopedService);
157+
158+
if (service && this !== globalContainer) {
159+
const clonedService = Object.assign({}, service);
160+
clonedService.value = undefined;
161+
const value = this.getServiceValueAsync(identifier, clonedService);
162+
this.set(identifier, value);
163+
return value;
164+
}
165+
166+
return this.getServiceValueAsync(identifier, service);
167+
}
168+
169+
/**
170+
* Like getMany, but returns a promise that recursively resolves async properties on all services.
171+
* Used when services defined with multiple: true and asyncInitialization: true flags.
172+
*/
173+
getManyAsync<T>(id: string): T[];
174+
175+
/**
176+
* Like getMany, but returns a promise that recursively resolves async properties on all services.
177+
* Used when services defined with multiple: true and asyncInitialization: true flags.
178+
*/
179+
getManyAsync<T>(id: Token<T>): T[];
180+
181+
/**
182+
* Like getMany, but returns a promise that recursively resolves async properties on all services.
183+
* Used when services defined with multiple: true and asyncInitialization: true flags.
184+
*/
185+
getManyAsync<T>(id: string | Token<T>): Promise<T>[] {
186+
return this.filterServices(id).map(service => this.getServiceValueAsync(id, service));
187+
}
188+
118189
/**
119190
* Gets all instances registered in the container of the given service identifier.
120191
* Used when service defined with multiple: true flag.
@@ -344,6 +415,100 @@ export class ContainerInstance {
344415
return value;
345416
}
346417

418+
/**
419+
* Gets a promise of an initialized AsyncService value.
420+
*/
421+
private async getServiceValueAsync(
422+
identifier: ServiceIdentifier,
423+
service: ServiceMetadata<any, any> | undefined
424+
): Promise<any> {
425+
// find if instance of this object already initialized in the container and return it if it is
426+
if (service && service.value !== undefined) return service.value;
427+
428+
// if named service was requested and its instance was not found plus there is not type to know what to initialize,
429+
// this means service was not pre-registered and we throw an exception
430+
if (
431+
(!service || !service.type) &&
432+
(!service || !service.factory) &&
433+
(typeof identifier === 'string' || identifier instanceof Token)
434+
)
435+
throw new ServiceNotFoundError(identifier);
436+
437+
// at this point we either have type in service registered, either identifier is a target type
438+
let type = undefined;
439+
if (service && service.type) {
440+
type = service.type;
441+
} else if (service && service.id instanceof Function) {
442+
type = service.id;
443+
} else if (identifier instanceof Function) {
444+
type = identifier;
445+
446+
// } else if (identifier instanceof Object && (identifier as { service: Token<any> }).service instanceof Token) {
447+
// type = (identifier as { service: Token<any> }).service;
448+
}
449+
450+
// if service was not found then create a new one and register it
451+
if (!service) {
452+
if (!type) throw new MissingProvidedServiceTypeError(identifier);
453+
454+
service = { type: type };
455+
this.services.push(service);
456+
}
457+
458+
// setup constructor parameters for a newly initialized service
459+
const paramTypes =
460+
type && Reflect && (Reflect as any).getMetadata
461+
? (Reflect as any).getMetadata('design:paramtypes', type)
462+
: undefined;
463+
let params: any[] = paramTypes ? await Promise.all(this.initializeParamsAsync(type, paramTypes)) : [];
464+
465+
// if factory is set then use it to create service instance
466+
let value: any;
467+
if (service.factory) {
468+
// filter out non-service parameters from created service constructor
469+
// non-service parameters can be, lets say Car(name: string, isNew: boolean, engine: Engine)
470+
// where name and isNew are non-service parameters and engine is a service parameter
471+
params = params.filter(param => param !== undefined);
472+
473+
if (service.factory instanceof Array) {
474+
// use special [Type, "create"] syntax to allow factory services
475+
// in this case Type instance will be obtained from Container and its method "create" will be called
476+
value = (await this.getAsync(service.factory[0]))[service.factory[1]](...params);
477+
} else {
478+
// regular factory function
479+
value = service.factory(...params, this);
480+
}
481+
} else {
482+
// otherwise simply create a new object instance
483+
if (!type) throw new MissingProvidedServiceTypeError(identifier);
484+
485+
params.unshift(null);
486+
487+
// "extra feature" - always pass container instance as the last argument to the service function
488+
// this allows us to support javascript where we don't have decorators and emitted metadata about dependencies
489+
// need to be injected, and user can use provided container to get instances he needs
490+
params.push(this);
491+
492+
// eslint-disable-next-line prefer-spread
493+
value = new (type.bind.apply(type, params))();
494+
}
495+
496+
if (service && !service.transient && value) service.value = value;
497+
498+
if (type) this.applyPropertyHandlers(type, value);
499+
500+
if (value instanceof AsyncInitializedService || service.asyncInitialization) {
501+
return new Promise((resolve) => {
502+
if (!(value.initialized instanceof Promise) && service.asyncInitialization) {
503+
throw new MissingInitializedPromiseError(service.value);
504+
}
505+
506+
value.initialized.then(() => resolve(value));
507+
});
508+
}
509+
return Promise.resolve(value);
510+
}
511+
347512
/**
348513
* Initializes all parameter types for a given target service class.
349514
*/
@@ -360,6 +525,22 @@ export class ContainerInstance {
360525
});
361526
}
362527

528+
/**
529+
* Returns array of promises for all initialized parameter types for a given target service class.
530+
*/
531+
private initializeParamsAsync(type: Function, paramTypes: any[]): Array<Promise<any> | undefined> {
532+
return paramTypes.map((paramType, index) => {
533+
const paramHandler = Container.handlers.find(handler => handler.object === type && handler.index === index);
534+
if (paramHandler) return Promise.resolve(paramHandler.value(this));
535+
536+
if (paramType && paramType.name && !this.isTypePrimitive(paramType.name)) {
537+
return this.getAsync(paramType);
538+
}
539+
540+
return undefined;
541+
});
542+
}
543+
363544
/**
364545
* Checks if given type is primitive (e.g. string, boolean, number, object).
365546
*/

src/container.class.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Constructable } from './types/constructable.type';
55
import { ServiceIdentifier } from './types/service-identifier.type';
66
import { ServiceMetadata } from './interfaces/service-metadata.interface.';
77

8+
89
/**
910
* Service container.
1011
*/
@@ -105,6 +106,38 @@ export class Container {
105106
return this.globalInstance.get(identifier as any);
106107
}
107108

109+
/**
110+
* Like get, but returns a promise of a service that recursively resolves async properties.
111+
* Used when service defined with asyncInitialization: true flag.
112+
*/
113+
static getAsync<T>(type: ObjectType<T>): Promise<T>;
114+
115+
/**
116+
* Like get, but returns a promise of a service that recursively resolves async properties.
117+
* Used when service defined with asyncInitialization: true flag.
118+
*/
119+
static getAsync<T>(id: string): Promise<T>;
120+
121+
/**
122+
* Like get, but returns a promise of a service that recursively resolves async properties.
123+
* Used when service defined with asyncInitialization: true flag.
124+
*/
125+
static getAsync<T>(id: Token<T>): Promise<T>;
126+
127+
/**
128+
* Like get, but returns a promise of a service that recursively resolves async properties.
129+
* Used when service defined with asyncInitialization: true flag.
130+
*/
131+
static getAsync<T>(service: { service: T }): Promise<T>;
132+
133+
/**
134+
* Like get, but returns a promise of a service that recursively resolves async properties.
135+
* Used when service defined with asyncInitialization: true flag.
136+
*/
137+
static getAsync<T>(identifier: ServiceIdentifier<T>): Promise<T> {
138+
return this.globalInstance.getAsync(identifier as any);
139+
}
140+
108141
/**
109142
* Gets all instances registered in the container of the given service identifier.
110143
* Used when service defined with multiple: true flag.
@@ -125,6 +158,26 @@ export class Container {
125158
return this.globalInstance.getMany(id as any);
126159
}
127160

161+
/**
162+
* Like getMany, but returns a promise that recursively resolves async properties on all services.
163+
* Used when services defined with multiple: true and asyncInitialization: true flags.
164+
*/
165+
static getManyAsync<T>(id: string): T[];
166+
167+
/**
168+
* Like getMany, but returns a promise that recursively resolves async properties on all services.
169+
* Used when services defined with multiple: true and asyncInitialization: true flags.
170+
*/
171+
static getManyAsync<T>(id: Token<T>): T[];
172+
173+
/**
174+
* Like getMany, but returns a promise that recursively resolves async properties on all services.
175+
* Used when services defined with multiple: true and asyncInitialization: true flags.
176+
*/
177+
static getManyAsync<T>(id: string | Token<T>): Promise<T>[] {
178+
return this.globalInstance.getManyAsync(id as any);
179+
}
180+
128181
/**
129182
* Sets a value for the given type or service name in the container.
130183
*/

src/decorators/service.decorator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,15 @@ export function Service<T, K extends keyof T>(
173173
service.multiple = (optionsOrServiceName as ServiceOptions<T, K>).multiple;
174174
service.global = (optionsOrServiceName as ServiceOptions<T, K>).global || false;
175175
service.transient = (optionsOrServiceName as ServiceOptions<T, K>).transient;
176+
service.asyncInitialization = (optionsOrServiceName as ServiceOptions<T, K>).asyncInitialization;
176177
} else if (optionsOrServiceName) {
177178
// ServiceOptions
178179
service.id = (optionsOrServiceName as ServiceOptions<T, K>).id;
179180
service.factory = (optionsOrServiceName as ServiceOptions<T, K>).factory;
180181
service.multiple = (optionsOrServiceName as ServiceOptions<T, K>).multiple;
181182
service.global = (optionsOrServiceName as ServiceOptions<T, K>).global || false;
182183
service.transient = (optionsOrServiceName as ServiceOptions<T, K>).transient;
184+
service.asyncInitialization = (optionsOrServiceName as ServiceOptions<T, K>).asyncInitialization;
183185
}
184186

185187
Container.set(service);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Thrown when user improperly uses the asyncInitialization Service option.
3+
*/
4+
export class MissingInitializedPromiseError extends Error {
5+
name = 'MissingInitializedPromiseError';
6+
7+
// TODO: User proper type
8+
constructor(value: { name: string; initialized: boolean }) {
9+
super(
10+
(value.initialized
11+
? `asyncInitialization: true was used, but ${value.name}#initialized is not a Promise. `
12+
: `asyncInitialization: true was used, but ${value.name}#initialized is undefined. `) +
13+
`You will need to either extend the abstract AsyncInitializedService class, or assign ` +
14+
`${value.name}#initialized to a Promise in your class' constructor that resolves when all required ` +
15+
`initialization is complete.`
16+
);
17+
Object.setPrototypeOf(this, MissingInitializedPromiseError.prototype);
18+
}
19+
}

src/interfaces/service-metadata.interface..ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export interface ServiceMetadata<T, K extends keyof T> {
4040
*/
4141
factory?: [Constructable<T>, K] | ((...params: any[]) => any);
4242

43+
/**
44+
* Will call instance's #initialize method and resolve the promise it returns when getting with Container.getAsync() and Container.getManyAsync().
45+
*/
46+
asyncInitialization?: boolean;
47+
4348
/**
4449
* Instance of the target class.
4550
*/

src/interfaces/service-options.interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ export interface ServiceOptions<T, K extends keyof T> {
3131
* Factory used to produce this service.
3232
*/
3333
factory?: [Constructable<T>, K] | ((...params: any[]) => any);
34+
35+
/**
36+
* Will call instance's #initialize method and resolve the promise it returns when getting with Container.getAsync() and Container.getManyAsync().
37+
*/
38+
asyncInitialization?: boolean;
3439
}

src/types/AsyncInitializedService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Extend when declaring a service with asyncInitialization: true flag.
3+
*/
4+
export abstract class AsyncInitializedService {
5+
public initialized: Promise<any>;
6+
7+
constructor() {
8+
this.initialized = this.initialize();
9+
}
10+
11+
protected abstract initialize(): Promise<any>;
12+
}

0 commit comments

Comments
 (0)