4
4
// License text available at https://opensource.org/licenses/MIT
5
5
6
6
import { Context } from './context' ;
7
- import { BoundValue , ValueOrPromise } from './binding' ;
7
+ import { BoundValue , ValueOrPromise , Binding } from './binding' ;
8
8
import { isPromise } from './is-promise' ;
9
9
import {
10
10
describeInjectedArguments ,
@@ -20,6 +20,68 @@ export type Constructor<T> =
20
20
// tslint:disable-next-line:no-any
21
21
new ( ...args : any [ ] ) => T ;
22
22
23
+ /**
24
+ * Object to keep states for a session to resolve bindings and their
25
+ * dependencies within a context
26
+ */
27
+ export class ResolutionSession {
28
+ /**
29
+ * A stack of bindings for the current resolution session. It's used to track
30
+ * the path of dependency resolution and detect circular dependencies.
31
+ */
32
+ readonly bindings : Binding [ ] = [ ] ;
33
+
34
+ /**
35
+ * Start to resolve a binding within the session
36
+ * @param binding Binding
37
+ * @param session Resolution session
38
+ */
39
+ static enterBinding (
40
+ binding : Binding ,
41
+ session ?: ResolutionSession ,
42
+ ) : ResolutionSession {
43
+ session = session || new ResolutionSession ( ) ;
44
+ session . enter ( binding ) ;
45
+ return session ;
46
+ }
47
+
48
+ /**
49
+ * Getter for the current binding
50
+ */
51
+ get binding ( ) {
52
+ return this . bindings [ this . bindings . length - 1 ] ;
53
+ }
54
+
55
+ /**
56
+ * Enter the resolution of the given binding. If
57
+ * @param binding Binding
58
+ */
59
+ enter ( binding : Binding ) {
60
+ if ( this . bindings . indexOf ( binding ) !== - 1 ) {
61
+ throw new Error (
62
+ `Circular dependency detected for '${
63
+ binding . key
64
+ } ' on path '${ this . getBindingPath ( ) } '`,
65
+ ) ;
66
+ }
67
+ this . bindings . push ( binding ) ;
68
+ }
69
+
70
+ /**
71
+ * Exit the resolution of a binding
72
+ */
73
+ exit ( ) {
74
+ return this . bindings . pop ( ) ;
75
+ }
76
+
77
+ /**
78
+ * Get the binding path as `bindingA->bindingB->bindingC`.
79
+ */
80
+ getBindingPath ( ) {
81
+ return this . bindings . map ( b => b . key ) . join ( '->' ) ;
82
+ }
83
+ }
84
+
23
85
/**
24
86
* Create an instance of a class which constructor has arguments
25
87
* decorated with `@inject`.
@@ -29,16 +91,19 @@ export type Constructor<T> =
29
91
*
30
92
* @param ctor The class constructor to call.
31
93
* @param ctx The context containing values for `@inject` resolution
94
+ * @param session Optional session for binding and dependency resolution
32
95
* @param nonInjectedArgs Optional array of args for non-injected parameters
33
96
*/
34
97
export function instantiateClass < T > (
35
98
ctor : Constructor < T > ,
36
99
ctx : Context ,
100
+ session ?: ResolutionSession ,
37
101
// tslint:disable-next-line:no-any
38
102
nonInjectedArgs ?: any [ ] ,
39
103
) : T | Promise < T > {
40
- const argsOrPromise = resolveInjectedArguments ( ctor , ctx , '' ) ;
41
- const propertiesOrPromise = resolveInjectedProperties ( ctor , ctx ) ;
104
+ session = session || new ResolutionSession ( ) ;
105
+ const argsOrPromise = resolveInjectedArguments ( ctor , '' , ctx , session ) ;
106
+ const propertiesOrPromise = resolveInjectedProperties ( ctor , ctx , session ) ;
42
107
let inst : T | Promise < T > ;
43
108
if ( isPromise ( argsOrPromise ) ) {
44
109
// Instantiate the class asynchronously
@@ -72,14 +137,19 @@ export function instantiateClass<T>(
72
137
* Resolve the value or promise for a given injection
73
138
* @param ctx Context
74
139
* @param injection Descriptor of the injection
140
+ * @param session Optional session for binding and dependency resolution
75
141
*/
76
- function resolve < T > ( ctx : Context , injection : Injection ) : ValueOrPromise < T > {
142
+ function resolve < T > (
143
+ ctx : Context ,
144
+ injection : Injection ,
145
+ session ?: ResolutionSession ,
146
+ ) : ValueOrPromise < T > {
77
147
if ( injection . resolve ) {
78
148
// A custom resolve function is provided
79
- return injection . resolve ( ctx , injection ) ;
149
+ return injection . resolve ( ctx , injection , session ) ;
80
150
}
81
151
// Default to resolve the value from the context by binding key
82
- return ctx . getValueOrPromise ( injection . bindingKey ) ;
152
+ return ctx . getValueOrPromise ( injection . bindingKey , session ) ;
83
153
}
84
154
85
155
/**
@@ -92,16 +162,18 @@ function resolve<T>(ctx: Context, injection: Injection): ValueOrPromise<T> {
92
162
*
93
163
* @param target The class for constructor injection or prototype for method
94
164
* injection
95
- * @param ctx The context containing values for `@inject` resolution
96
165
* @param method The method name. If set to '', the constructor will
97
166
* be used.
167
+ * @param ctx The context containing values for `@inject` resolution
168
+ * @param session Optional session for binding and dependency resolution
98
169
* @param nonInjectedArgs Optional array of args for non-injected parameters
99
170
*/
100
171
export function resolveInjectedArguments (
101
172
// tslint:disable-next-line:no-any
102
173
target : any ,
103
- ctx : Context ,
104
174
method : string ,
175
+ ctx : Context ,
176
+ session ?: ResolutionSession ,
105
177
// tslint:disable-next-line:no-any
106
178
nonInjectedArgs ?: any [ ] ,
107
179
) : BoundValue [ ] | Promise < BoundValue [ ] > {
@@ -137,7 +209,7 @@ export function resolveInjectedArguments(
137
209
}
138
210
}
139
211
140
- const valueOrPromise = resolve ( ctx , injection ) ;
212
+ const valueOrPromise = resolve ( ctx , injection , session ) ;
141
213
if ( isPromise ( valueOrPromise ) ) {
142
214
if ( ! asyncResolvers ) asyncResolvers = [ ] ;
143
215
asyncResolvers . push (
@@ -173,8 +245,9 @@ export function invokeMethod(
173
245
) : ValueOrPromise < BoundValue > {
174
246
const argsOrPromise = resolveInjectedArguments (
175
247
target ,
176
- ctx ,
177
248
method ,
249
+ ctx ,
250
+ undefined ,
178
251
nonInjectedArgs ,
179
252
) ;
180
253
assert ( typeof target [ method ] === 'function' , `Method ${ method } not found` ) ;
@@ -189,11 +262,24 @@ export function invokeMethod(
189
262
190
263
export type KV = { [ p : string ] : BoundValue } ;
191
264
265
+ /**
266
+ * Given a class with properties decorated with `@inject`,
267
+ * return the map of properties resolved using the values
268
+ * bound in `ctx`.
269
+
270
+ * The function returns an argument array when all dependencies were
271
+ * resolved synchronously, or a Promise otherwise.
272
+ *
273
+ * @param constructor The class for which properties should be resolved.
274
+ * @param ctx The context containing values for `@inject` resolution
275
+ * @param session Optional session for binding and dependency resolution
276
+ */
192
277
export function resolveInjectedProperties (
193
- fn : Function ,
278
+ constructor : Function ,
194
279
ctx : Context ,
280
+ session ?: ResolutionSession ,
195
281
) : KV | Promise < KV > {
196
- const injectedProperties = describeInjectedProperties ( fn . prototype ) ;
282
+ const injectedProperties = describeInjectedProperties ( constructor . prototype ) ;
197
283
198
284
const properties : KV = { } ;
199
285
let asyncResolvers : Promise < void > [ ] | undefined = undefined ;
@@ -205,11 +291,12 @@ export function resolveInjectedProperties(
205
291
const injection = injectedProperties [ p ] ;
206
292
if ( ! injection . bindingKey && ! injection . resolve ) {
207
293
throw new Error (
208
- `Cannot resolve injected property for class ${ fn . name } : ` +
294
+ `Cannot resolve injected property for class ${ constructor . name } : ` +
209
295
`The property ${ p } was not decorated for dependency injection.` ,
210
296
) ;
211
297
}
212
- const valueOrPromise = resolve ( ctx , injection ) ;
298
+
299
+ const valueOrPromise = resolve ( ctx , injection , session ) ;
213
300
if ( isPromise ( valueOrPromise ) ) {
214
301
if ( ! asyncResolvers ) asyncResolvers = [ ] ;
215
302
asyncResolvers . push ( valueOrPromise . then ( propertyResolver ( p ) ) ) ;
0 commit comments