-
Notifications
You must be signed in to change notification settings - Fork 0
/
container.ts
176 lines (160 loc) · 5.15 KB
/
container.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { BindingNotFoundError } from "./utils";
/**
* ID type used for binding and injecting components.
*/
export type ID = string | symbol;
/**
* Type of the injector function passed to component factories as the first argument
*/
export type Inject = <T>(id: ID) => T;
/**
* Utility type to derive the component factory type from a component type
*/
type FactoryOf<T> = (inject: Inject) => T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyFactory = FactoryOf<any>;
export class Container {
/**
* Array of parent containers
*
* These allow setting up parent-child relationships between containers, thus enabling
* hierarchical dependency injection systems. Multiple parents are supported, so you can essentially
* make your container "inherit" from several other containers
*/
public parents: Container[] = [];
private readonly bindings = new Map<ID, AnyFactory>();
/**
* Register an id with a component in the container.
*
* You can make use of the generic type on this method to enforce that the
* registered component matches the required interface.
*
* Example:
* ```ts
* container.register<IMyComponent>(MY_COMPONENT, myComponent)
* ```
* ---
* The registered binding can later be injected with the `inject` function like so:
* ```ts
* const component = inject<IMyComponent>(MY_COMPONENT);
* ```
* It is suggested you keep your dependency ids and types close to each other,
* preferably in a separate `bindings` file. That makes them easy to use and improves
* maintainability.
*/
public register<T>(id: ID, value: FactoryOf<T>): this {
this.bindings.set(id, value);
return this;
}
/**
* Check if there is a binding registered for a given id.
* This will check this container and also all of it's parents.
*
* Example:
* ```ts
* const myComponentIsRegistered = container.isRegistered(MY_COMPONENT)
* ```
*/
public isRegistered(id: ID): boolean {
return (
this.isRegisteredHere(id) ||
this.parents.some((parent) => parent.isRegistered(id))
);
}
/**
* Check if there is a binding registered for a given id.
* This will check only this container.
*
* Example:
* ```ts
* const myComponentIsRegistered = container.isRegisteredHere(MY_COMPONENT)
* ```
*/
public isRegisteredHere(id: ID): boolean {
return this.bindings.has(id);
}
/**
* Removes the binding for the given id.
* This will only remove it from this container.
*
* Example:
* ```ts
* container.remove(MY_COMPONENT)
* ```
*/
public remove(id: ID): this {
this.bindings.delete(id);
return this;
}
private _get(id: ID): AnyFactory | undefined {
if (this.bindings.has(id)) return this.bindings.get(id);
for (let i = 0; i < this.parents.length; i += 1) {
const binding = this.parents[i]._get(id);
if (binding !== undefined) return binding;
}
}
/**
* Get a binding from the container
*
* The binding will be first looked for in this container.
* If it's not found here, it will be looked for in parents, in their order in the `parents` array.
* - If the binding is found then its initialized and returned.
* - If it's not found then a `BindingNotFoundError`
* is thrown
*
* Example:
* ```ts
* const component = container.get<IMyComponent>(MY_COMPONENT);
* ```
*/
public get<T>(id: ID): T {
const binding = this._get(id);
if (binding === undefined) throw new BindingNotFoundError(id);
return binding(this.get.bind(this));
}
/**
* Extends the container's array of parents with the given containers.
* This makes the given containers' contents available to this container,
* effectively creating a parent-child relationship.
*
* For example, if some components in your container depend on some components
* in another container, then you should extend your container with that other container,
* to make those dependencies available for your components.
*
* This will append to the list of parents and not overwrite it.
* A new parent is only added if it doesn't already exist in the `parents` array.
*
* Example:
* ```ts
* container.extend(otherContainer1, otherContainer2)
* ```
*/
public extend(...containers: Container[]): this {
containers.forEach((container) => {
if (!this.parents.includes(container)) {
this.parents.push(container);
}
});
return this;
}
/**
* Creates and returns a child container.
*
* This is effectively the reverse of extending.
* The new container will have this container as the only parent.
*
* Child containers are very useful when you want to bind something for a single run,
* for example, if you've got request context you want to bind to the container before getting your component.
* Using child containers allows you to bind these temporary values without polluting the root container.
*
* Example:
* ```ts
* const child = container.createChild()
* ```
*/
public createChild(): Container {
const childContainer = new Container();
childContainer.extend(this);
return childContainer;
}
}