Skip to content

Commit

Permalink
refactor: remove async api and add lazy option (#42)
Browse files Browse the repository at this point in the history
* refactor: add lazy option and remove async api

* feat: add no throw option

* feat: inject suppot nothrow

* chore: lint code

* refactor: lazy implemention

* fix: no use object as type
  • Loading branch information
JerrysShan authored Jul 29, 2022
1 parent 4a982f5 commit ad79d40
Show file tree
Hide file tree
Showing 17 changed files with 138 additions and 269 deletions.
2 changes: 1 addition & 1 deletion src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export const INJECT_HANDLER_PROPS = Symbol.for('injection:handler_props');

export const INJECT_HANDLER_ARGS = Symbol.for('injection:handler_args');

export const MAP_TYPE = Symbol('map_type');
export const LAZY_HANDLER = Symbol('injection:lazy_handler');
102 changes: 14 additions & 88 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
CLASS_TAG,
INJECT_HANDLER_ARGS,
INJECT_HANDLER_PROPS,
MAP_TYPE,
LAZY_HANDLER,
} from './constant';
import {
Constructable,
ContainerType,
InjectOptions,
Identifier,
InjectableMetadata,
InjectableDefinition,
Expand All @@ -35,6 +36,7 @@ import {
NoIdentifierError,
InjectionError,
} from './error';
import { lazyHandler } from './lazy_helper';

export default class Container implements ContainerType {
private registry: Map<Identifier, InjectableMetadata>;
Expand All @@ -48,26 +50,20 @@ export default class Container implements ContainerType {
this.registry = new Map();
this.tags = new Map();
this.handlerMap = new Map();
this.registerHandler(LAZY_HANDLER, lazyHandler);
}

public get<T = unknown>(id: Identifier<T>): T {
const md = this.getMetadata(id);
public get<T = unknown>(id: Identifier<T>, options: InjectOptions = {}): T {
const md = this.getDefinition(id);
if (!md) {
if (options.noThrow) {
return options.defaultValue;
}
throw new NotFoundError(id);
}
return this.getValue(md);
}

public async getAsync<T = unknown>(id: Identifier<T>): Promise<T> {
const md = this.getMetadata(id);
if (!md) {
throw new NotFoundError(id);
}
const instance = await this.getValueAsync(md);
await instance[md.initMethod!]?.();
return instance;
}

public set(options: Partial<InjectableDefinition>) {
if (options.id && !isUndefined(options.value)) {
const md: InjectableMetadata = {
Expand Down Expand Up @@ -100,10 +96,9 @@ export default class Container implements ContainerType {
md.properties = (props ?? []).concat(handlerProps ?? []);
md.initMethod = initMethodMd?.propertyName ?? 'init';
/**
* compatible with inject type identifier when identifier is string
*/
* compatible with inject type identifier when identifier is string
*/
if (md.id !== type) {
md[MAP_TYPE] = type;
this.registry.set(type, md);
}

Expand All @@ -112,15 +107,14 @@ export default class Container implements ContainerType {

this.registry.set(md.id, md);
if (md.eager && md.scope !== ScopeEnum.TRANSIENT) {
// TODO: handle async
this.get(md.id);
}

return this;
}

public getDefinition<T = unknown>(id: Identifier<T>): InjectableMetadata<T> | undefined {
return this.getMetadata(id);
return this.registry.get(id);
}

public getInjectableByTag(tag: string): any[] {
Expand All @@ -133,11 +127,6 @@ export default class Container implements ContainerType {
return clazzes.map(clazz => this.get(clazz));
}

public getByTagAsync(tag: string) {
const clazzes = this.getInjectableByTag(tag);
return Promise.all(clazzes.map(clazz => this.getAsync(clazz)));
}

public registerHandler(name: string | symbol, handler: HandlerFunction) {
this.handlerMap.set(name, handler);
}
Expand Down Expand Up @@ -174,36 +163,6 @@ export default class Container implements ContainerType {
return value;
}

protected async getValueAsync(md: InjectableMetadata) {
if (!isUndefined(md.value)) {
return md.value;
}
let value;
if (md.factory) {
value = await md.factory(md.id, this);
}

if (!value && md.type) {
const clazz = md.type!;
const params = await this.resolveParamsAsync(clazz, md.constructorArgs);
value = new clazz(...params);
await this.handlePropsAsync(value, md.properties ?? []);
}

if (md.scope === ScopeEnum.SINGLETON) {
md.value = value;
}
return value;
}

protected getMetadata(id: Identifier): InjectableMetadata | undefined {
const md = this.registry.get(id);
if (md && md[MAP_TYPE]) {
return this.registry.get(md[MAP_TYPE]);
}
return md;
}

private getDefinedMetaData(options: Partial<InjectableDefinition>): {
id: Identifier;
scope: ScopeEnum;
Expand Down Expand Up @@ -253,52 +212,19 @@ export default class Container implements ContainerType {

params[arg.index!] = arg.handler
? this.resolveHandler(arg.handler, arg.id)
: this.get(arg.id);
: this.get(arg.id, { noThrow: arg.noThrow, defaultValue: arg.defaultValue });
});
return params;
}

private async resolveParamsAsync(clazz: any, args?: ReflectMetadataType[]) {
const params: any[] = [];
if (!args || !args.length) {
args = (getParamMetadata(clazz) ?? []).map((ele, index) => ({
id: ele,
index,
}));
}

await Promise.all(
args!.map(async arg => {
if (isPrimitiveFunction(arg.id)) {
return;
}

params[arg.index!] = arg.handler
? await this.resolveHandler(arg.handler, arg.id)
: await this.getAsync(arg.id);
}),
);
return params;
}

private handleProps(instance: any, props: ReflectMetadataType[]) {
props.forEach(prop => {
instance[prop.propertyName!] = prop.handler
? this.resolveHandler(prop.handler, prop.id)
: this.get(prop.id);
: this.get(prop.id, { noThrow: prop.noThrow, defaultValue: prop.defaultValue });
});
}

private async handlePropsAsync(instance: any, props: ReflectMetadataType[]) {
await Promise.all(
props.map(async prop => {
instance[prop.propertyName!] = prop.handler
? await this.resolveHandler(prop.handler, prop.id)
: await this.getAsync(prop.id);
}),
);
}

private handleTag(target: any) {
let tags = Reflect.getOwnMetadata(CLASS_TAG, target);
if (!tags) {
Expand Down
11 changes: 0 additions & 11 deletions src/decorator/init.ts

This file was deleted.

37 changes: 29 additions & 8 deletions src/decorator/inject.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { Identifier, ReflectMetadataType } from '../types';
import { setMetadata, getMetadata, isNumber, getDesignTypeMetadata, getParamMetadata, isPrimitiveFunction, isObject, isUndefined } from '../util';
import { CLASS_PROPERTY, CLASS_CONSTRUCTOR_ARGS } from '../constant';
import { InjectOptions, Identifier, ReflectMetadataType } from '../types';
import {
setMetadata,
getMetadata,
isNumber,
getDesignTypeMetadata,
getParamMetadata,
isPrimitiveFunction,
isObject,
isUndefined,
} from '../util';
import { CLASS_PROPERTY, CLASS_CONSTRUCTOR_ARGS, LAZY_HANDLER } from '../constant';
import { CannotInjectValueError } from '../error';


export function Inject(id?: Identifier) {
export function Inject(id?: Identifier);
export function Inject(options?: InjectOptions);
export function Inject(idOrOptions?: Identifier | InjectOptions) {
const options = (isObject(idOrOptions) ? idOrOptions : { id: idOrOptions }) as InjectOptions;
return (target: any, propertyKey: string | symbol, index?: number) => {
if (isObject(target)) {
target = target.constructor;
}
let propertyType = id;
let propertyType = options.id;
if (!propertyType && propertyKey) {
propertyType = getDesignTypeMetadata(target.prototype, propertyKey);
}
Expand All @@ -25,13 +36,23 @@ export function Inject(id?: Identifier) {

if (!isUndefined(index)) {
const metadata = (getMetadata(CLASS_CONSTRUCTOR_ARGS, target) || []) as ReflectMetadataType[];
metadata.push({ id: propertyType!, index });
metadata.push({
...options,
id: propertyType!,
index,
handler: options.lazy ? LAZY_HANDLER : undefined,
});
setMetadata(CLASS_CONSTRUCTOR_ARGS, metadata, target);
return;
}

const metadata = (getMetadata(CLASS_PROPERTY, target) || []) as ReflectMetadataType[];
metadata.push({ id: propertyType!, propertyName: propertyKey });
metadata.push({
...options,
id: propertyType!,
propertyName: propertyKey,
handler: options.lazy ? LAZY_HANDLER : undefined,
});
setMetadata(CLASS_PROPERTY, metadata, target);
};
}
2 changes: 1 addition & 1 deletion src/decorator/injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { CLASS_CONSTRUCTOR } from '../constant';

export function Injectable(options?: InjectableOption): ClassDecorator {
return (target: any) => {
setMetadata(CLASS_CONSTRUCTOR, { id: options?.id || target, scope: options?.scope ?? ScopeEnum.SINGLETON }, target);
setMetadata(CLASS_CONSTRUCTOR, { id: target, scope: ScopeEnum.SINGLETON, ...options }, target);
};
}
16 changes: 1 addition & 15 deletions src/execution_container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class ExecutionContainer extends Container {
}

public get<T = unknown>(id: Identifier<T>): T {
const md = this.getMetadata(id) ?? this.parent.getDefinition(id);
const md = this.getDefinition(id) ?? this.parent.getDefinition(id);
if (!md) {
throw new NotFoundError(id);
}
Expand All @@ -26,20 +26,6 @@ export default class ExecutionContainer extends Container {
return value;
}

public async getAsync<T = unknown>(id: Identifier<T>): Promise<T> {
const md = this.getMetadata(id) ?? this.parent.getDefinition(id);
if (!md) {
throw new NotFoundError(id);
}
const instance = await this.getValueAsync(md);

await instance[md.initMethod!]?.();
if (md.scope === ScopeEnum.EXECUTION) {
this.setValue(md, instance);
}
return instance;
}

public getCtx(): any {
return this.ctx;
}
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import ExecutionContainer from './execution_container';

export { Container, ExecutionContainer };
export * from './types';
export * from './decorator/init';
export * from './decorator/inject';
export * from './decorator/injectable';
export * from './decorator/handler';
Expand Down
35 changes: 35 additions & 0 deletions src/lazy_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Container from './container';
import { Identifier } from './types';

const reflectMethods: ReadonlyArray<keyof ProxyHandler<any>> = ['get', 'set', 'getPrototypeOf'];

function createHandler<T extends Object>(delayedObject: () => T): ProxyHandler<T> {
const handler: ProxyHandler<T> = {};
const install = (name: keyof ProxyHandler<T>) => {
handler[name] = (...args: any[]) => {
args[0] = delayedObject();
const method = Reflect[name];
return (method as any)(...args);
};
};
reflectMethods.forEach(install);
return handler;
}

function createProxy<T>(id: Identifier<T>, container: Container): T {
const target: Record<string, any> = {};
let init = false;
let value: T;
const delayedObject: () => T = (): T => {
if (!init) {
value = container.get(id);
init = true;
}
return value;
};
return new Proxy<any>(target, createHandler(delayedObject)) as T;
}

export function lazyHandler<T>(id: Identifier<T>, container: Container): T {
return createProxy(id, container);
}
Loading

0 comments on commit ad79d40

Please sign in to comment.