Skip to content

Commit

Permalink
feat: add createChildContainer (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
siyul-park committed Mar 20, 2021
1 parent 7055b71 commit bb49ea7
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 11 deletions.
37 changes: 37 additions & 0 deletions packages/cheeket/lib/binding/combined-binding-dictionary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as interfaces from "../interfaces";

class CombinedBindingDictionary implements interfaces.BindingDictionary {
readonly #bindingDictionaries: interfaces.BindingDictionary[] = [];

constructor(bindingDictionaries: interfaces.BindingDictionary[]) {
this.#bindingDictionaries = bindingDictionaries;
}

get<T>(token: interfaces.Token<T>): interfaces.Provider<T> | undefined {
// eslint-disable-next-line no-restricted-syntax
for (const bindingDictionary of this.#bindingDictionaries) {
const provider = bindingDictionary.get(token);
if (provider !== undefined) return provider;
}

return undefined;
}

getAll<T>(token: interfaces.Token<T>): interfaces.Provider<T>[] {
const providers: interfaces.Provider<T>[] = [];

this.#bindingDictionaries.forEach((bindingDictionary) => {
providers.push(...bindingDictionary.getAll(token));
});

return providers;
}

has<T>(token: interfaces.Token<T>): boolean {
return this.#bindingDictionaries.some((bindingDictionary) =>
bindingDictionary.has(token)
);
}
}

export default CombinedBindingDictionary;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as interfaces from "../interfaces";

class BindingDictionary implements interfaces.BindingDictionary {
class MutableBindingDictionary implements interfaces.MutableBindingDictionary {
readonly #storage = new Map<
interfaces.Token<unknown>,
interfaces.Provider<unknown>[]
Expand Down Expand Up @@ -34,4 +34,4 @@ class BindingDictionary implements interfaces.BindingDictionary {
}
}

export default BindingDictionary;
export default MutableBindingDictionary;
91 changes: 91 additions & 0 deletions packages/cheeket/lib/container/child-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { EventEmitter2 } from "eventemitter2";

import * as interfaces from "../interfaces";
import Context from "../context/context";
import MutableBindingDictionary from "../binding/mutable-binding-dictionary";
import CombinedBindingDictionary from "../binding/combined-binding-dictionary";
import Request from "../context/request";
import CantResolveError from "../error/cant-resolve-error";
import { EventType } from "../event";

class ChildContainer extends EventEmitter2 implements interfaces.Container {
readonly #bindingDictionary: interfaces.MutableBindingDictionary = new MutableBindingDictionary();

readonly #parentBindingDictionary: interfaces.BindingDictionary;

readonly #combinedBindingDictionary: interfaces.BindingDictionary;

constructor(parentBindingDictionary: interfaces.BindingDictionary) {
super();

this.#parentBindingDictionary = parentBindingDictionary;
this.#combinedBindingDictionary = new CombinedBindingDictionary([
this.#parentBindingDictionary,
this.#bindingDictionary,
]);
}

bind<T>(token: interfaces.Token<T>, provider: interfaces.Provider<T>): void {
this.#bindingDictionary.set(token, provider);
}

isBound<T>(token: interfaces.Token<T>): boolean {
return this.#combinedBindingDictionary.has(token);
}

rebind<T>(
token: interfaces.Token<T>,
provider: interfaces.Provider<T>
): void {
this.#bindingDictionary.delete(token);
this.#bindingDictionary.set(token, provider);
}

unbind<T>(token: interfaces.Token<T>): void {
this.#bindingDictionary.delete(token);
}

async resolve<T>(token: interfaces.Token<T>): Promise<T> {
const provider = this.#combinedBindingDictionary.get(token);
if (provider !== undefined) {
return this.resolveProvider(provider, token);
}

throw new CantResolveError(token, this);
}

async resolveAll<T>(token: interfaces.Token<T>): Promise<T[]> {
const providers = this.#combinedBindingDictionary.getAll(token);
if (providers.length > 0) {
return Promise.all(
providers.map((provider) => this.resolveProvider(provider, token))
);
}

throw new CantResolveError(token, this);
}

private async resolveProvider<T>(
provider: interfaces.Provider<T>,
token: interfaces.Token<T>
): Promise<T> {
const context = this.createContext(token);
const value = await provider(context);
context.request.resolved = value;

await this.emitAsync(EventType.Resolve, context);

return value;
}

private createContext<T>(token: interfaces.Token<T>): interfaces.Context {
const request = new Request(token);
return new Context(this.#combinedBindingDictionary, this, request);
}

createChildContainer(): interfaces.Container {
return new ChildContainer(this.#combinedBindingDictionary);
}
}

export default ChildContainer;
9 changes: 7 additions & 2 deletions packages/cheeket/lib/container/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { EventEmitter2 } from "eventemitter2";

import * as interfaces from "../interfaces";
import Context from "../context/context";
import BindingDictionary from "../binding/binding-dictionary";
import MutableBindingDictionary from "../binding/mutable-binding-dictionary";
import Request from "../context/request";
import CantResolveError from "../error/cant-resolve-error";
import { EventType } from "../event";
import ChildContainer from "./child-container";

class Container extends EventEmitter2 implements interfaces.Container {
readonly #bindingDictionary: interfaces.BindingDictionary = new BindingDictionary();
readonly #bindingDictionary: interfaces.MutableBindingDictionary = new MutableBindingDictionary();

bind<T>(token: interfaces.Token<T>, provider: interfaces.Provider<T>): void {
this.#bindingDictionary.set(token, provider);
Expand Down Expand Up @@ -67,6 +68,10 @@ class Container extends EventEmitter2 implements interfaces.Container {
const request = new Request(token);
return new Context(this.#bindingDictionary, this, request);
}

createChildContainer(): interfaces.Container {
return new ChildContainer(this.#bindingDictionary);
}
}

export default Container;
2 changes: 0 additions & 2 deletions packages/cheeket/lib/interfaces/binding-dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import Token from "./token";
import Provider from "./provider";

interface BindingDictionary {
set<T>(token: Token<T>, provider: Provider<T>): void;
get<T>(token: Token<T>): Provider<T> | undefined;
getAll<T>(token: Token<T>): Provider<T>[];
delete<T>(token: Token<T>): void;
has<T>(token: Token<T>): boolean;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/cheeket/lib/interfaces/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Resolver from "./resolver";
import Binder from "./binder";
import EventProducer from "./event-producer";

interface Container extends Resolver, Binder, EventEmitter2, EventProducer {}
interface Container extends Resolver, Binder, EventEmitter2, EventProducer {
createChildContainer(): Container;
}

export default Container;
2 changes: 2 additions & 0 deletions packages/cheeket/lib/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export { default as Abstract } from "./abstract";
export { default as Binder } from "./binder";
export { default as BindingDictionary } from "./binding-dictionary";
export { default as MutableBindingDictionary } from "./mutable-binding-dictionary";
export { default as Container } from "./container";
export { default as Context } from "./context";
export { default as Provider } from "./provider";
export { default as ScopeProvider } from "./scope-provider";
export { default as Request } from "./request";
export { default as Resolver } from "./resolver";
export { default as Token } from "./token";
Expand Down
13 changes: 13 additions & 0 deletions packages/cheeket/lib/interfaces/mutable-binding-dictionary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Token from "./token";
import Provider from "./provider";
import BindingDictionary from "./binding-dictionary";

interface MutableBindingDictionary extends BindingDictionary {
set<T>(token: Token<T>, provider: Provider<T>): void;
get<T>(token: Token<T>): Provider<T> | undefined;
getAll<T>(token: Token<T>): Provider<T>[];
delete<T>(token: Token<T>): void;
has<T>(token: Token<T>): boolean;
}

export default MutableBindingDictionary;
7 changes: 7 additions & 0 deletions packages/cheeket/lib/interfaces/scope-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Provider from "./provider";

interface ScopeProvider<T> extends Provider<T> {
clear(): void;
}

export default ScopeProvider;
10 changes: 8 additions & 2 deletions packages/cheeket/lib/provider/in-request-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import { EventType } from "../event";

function inRequestScope<T>(
provider: interfaces.Provider<T>
): interfaces.Provider<T> {
return async (context: interfaces.Context) => {
): interfaces.ScopeProvider<T> {
const scopeProvider: Partial<interfaces.ScopeProvider<T>> = async (
context: interfaces.Context
) => {
const value = await provider(context);
await context.emitAsync(EventType.Create, value, context);
return value;
};

scopeProvider.clear = () => {};

return scopeProvider as interfaces.ScopeProvider<T>;
}

export default inRequestScope;
13 changes: 11 additions & 2 deletions packages/cheeket/lib/provider/in-singleton-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { EventType } from "../event";

function inSingletonScope<T>(
provider: interfaces.Provider<T>
): interfaces.Provider<T> {
): interfaces.ScopeProvider<T> {
let cache: T | undefined;
return async (context: interfaces.Context) => {

const scopeProvider: Partial<interfaces.ScopeProvider<T>> = async (
context: interfaces.Context
) => {
if (cache === undefined) {
const value = await provider(context);
await context.emitAsync(EventType.Create, value, context);
Expand All @@ -14,6 +17,12 @@ function inSingletonScope<T>(

return cache;
};

scopeProvider.clear = () => {
cache = undefined;
};

return scopeProvider as interfaces.ScopeProvider<T>;
}

export default inSingletonScope;
20 changes: 20 additions & 0 deletions packages/cheeket/test/unit/container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,23 @@ test("resolve event", async () => {
expect(contexts[1].request.resolved).not.toBeUndefined();
expect(contexts[2].request.resolved).not.toBeUndefined();
});

test("createChildContainer", async () => {
const container = new Container();

container.bind(Types.Weapon, inRequestScope(katanaProvider));
container.bind(Types.ThrowableWeapon, inRequestScope(shurikenProvider));

const childContainer = container.createChildContainer();

childContainer.bind(Types.Warrior, inRequestScope(ninjaProvider));

const warrior = await childContainer.resolve<Warrior>(Types.Warrior);
const throwableWeapon = await container.resolve<ThrowableWeapon>(
Types.ThrowableWeapon
);
const weapon = await container.resolve<Weapon>(Types.Weapon);

expect(warrior.fight()).toEqual(weapon.hit());
expect(warrior.sneak()).toEqual(throwableWeapon.throw());
});

0 comments on commit bb49ea7

Please sign in to comment.