Skip to content

Commit b2f7eda

Browse files
committed
feat(context): use one stack to track bindings and injections
1 parent 4ea0485 commit b2f7eda

File tree

4 files changed

+125
-38
lines changed

4 files changed

+125
-38
lines changed

packages/context/src/inject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ function resolveAsGetter(
193193
session?: ResolutionSession,
194194
) {
195195
// We need to clone the session for the getter as it will be resolved later
196-
if (session != null) session = session.clone();
196+
session = ResolutionSession.fork(session);
197197
return function getter() {
198198
return ctx.get(injection.bindingKey, session);
199199
};

packages/context/src/resolution-session.ts

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ function tryWithFinally(
5656
return result;
5757
}
5858

59+
/**
60+
* Wrapper for bindings tracked by resolution sessions
61+
*/
62+
export interface BindingElement {
63+
type: 'binding';
64+
value: Binding;
65+
}
66+
67+
/**
68+
* Wrapper for injections tracked by resolution sessions
69+
*/
70+
export interface InjectionElement {
71+
type: 'injection';
72+
value: Injection;
73+
}
74+
75+
/**
76+
* Binding or injection elements tracked by resolution sessions
77+
*/
78+
export type ResolutionElement = BindingElement | InjectionElement;
79+
5980
/**
6081
* Object to keep states for a session to resolve bindings and their
6182
* dependencies within a context
@@ -65,28 +86,25 @@ export class ResolutionSession {
6586
* A stack of bindings for the current resolution session. It's used to track
6687
* the path of dependency resolution and detect circular dependencies.
6788
*/
68-
readonly bindings: Binding[] = [];
69-
70-
/**
71-
* A stack of injections for the current resolution session.
72-
*/
73-
readonly injections: Injection[] = [];
89+
readonly stack: ResolutionElement[] = [];
7490

7591
/**
76-
* Take a snapshot of the ResolutionSession so that we can pass it to
77-
* `@inject.getter` without interferring with the current session
92+
* Fork the current session so that a new one with the same stack can be used
93+
* in parallel or future resolutions, such as multiple method arguments,
94+
* multiple properties, or a getter function
95+
* @param session The current session
7896
*/
79-
clone() {
97+
static fork(session?: ResolutionSession): ResolutionSession | undefined {
98+
if (session === undefined) return undefined;
8099
const copy = new ResolutionSession();
81-
copy.bindings.push(...this.bindings);
82-
copy.injections.push(...this.injections);
100+
copy.stack.push(...session.stack);
83101
return copy;
84102
}
85103

86104
/**
87105
* Start to resolve a binding within the session
88-
* @param binding Binding
89-
* @param session Resolution session
106+
* @param binding The current binding
107+
* @param session The current resolution session
90108
*/
91109
private static enterBinding(
92110
binding: Binding,
@@ -117,8 +135,8 @@ export class ResolutionSession {
117135

118136
/**
119137
* Push an injection into the session
120-
* @param injection Injection
121-
* @param session Resolution session
138+
* @param injection The current injection
139+
* @param session The current resolution session
122140
*/
123141
private static enterInjection(
124142
injection: Injection,
@@ -152,7 +170,7 @@ export class ResolutionSession {
152170

153171
/**
154172
* Describe the injection for debugging purpose
155-
* @param injection
173+
* @param injection Injection object
156174
*/
157175
static describeInjection(injection?: Injection) {
158176
/* istanbul ignore if */
@@ -171,7 +189,7 @@ export class ResolutionSession {
171189

172190
/**
173191
* Push the injection onto the session
174-
* @param injection Injection
192+
* @param injection Injection The current injection
175193
*/
176194
pushInjection(injection: Injection) {
177195
/* istanbul ignore if */
@@ -181,41 +199,60 @@ export class ResolutionSession {
181199
ResolutionSession.describeInjection(injection),
182200
);
183201
}
184-
this.injections.push(injection);
202+
this.stack.push({type: 'injection', value: injection});
185203
/* istanbul ignore if */
186204
if (debugSession.enabled) {
187-
debugSession('Injection path:', this.getInjectionPath());
205+
debugSession('Resolution path:', this.getResolutionPath());
188206
}
189207
}
190208

191209
/**
192210
* Pop the last injection
193211
*/
194212
popInjection() {
195-
const injection = this.injections.pop();
213+
const top = this.stack.pop();
214+
if (top === undefined || top.type !== 'injection') {
215+
throw new Error('The top element must be an injection');
216+
}
217+
218+
const injection = top.value;
196219
/* istanbul ignore if */
197220
if (debugSession.enabled) {
198221
debugSession(
199222
'Exit injection:',
200223
ResolutionSession.describeInjection(injection),
201224
);
202-
debugSession('Injection path:', this.getInjectionPath() || '<empty>');
225+
debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
203226
}
204227
return injection;
205228
}
206229

207230
/**
208231
* Getter for the current injection
209232
*/
210-
get currentInjection() {
211-
return this.injections[this.injections.length - 1];
233+
get currentInjection(): Injection | undefined {
234+
for (let i = this.stack.length - 1; i >= 0; i--) {
235+
const element = this.stack[i];
236+
switch (element.type) {
237+
case 'injection':
238+
return element.value;
239+
}
240+
}
241+
return undefined;
212242
}
213243

214244
/**
215245
* Getter for the current binding
216246
*/
217-
get currentBinding() {
218-
return this.bindings[this.bindings.length - 1];
247+
get currentBinding(): Binding | undefined {
248+
for (let i = this.stack.length - 1; i >= 0; i--) {
249+
const element = this.stack[i];
250+
switch (element.type) {
251+
case 'binding':
252+
return element.value;
253+
}
254+
}
255+
return undefined;
219256
}
220257

221258
/**
@@ -227,29 +264,33 @@ export class ResolutionSession {
227264
if (debugSession.enabled) {
228265
debugSession('Enter binding:', binding.toJSON());
229266
}
230-
if (this.bindings.indexOf(binding) !== -1) {
267+
if (this.stack.find(i => i.type === 'binding' && i.value === binding)) {
231268
throw new Error(
232269
`Circular dependency detected on path '${this.getBindingPath()} --> ${
233270
binding.key
234271
}'`,
235272
);
236273
}
237-
this.bindings.push(binding);
274+
this.stack.push({type: 'binding', value: binding});
238275
/* istanbul ignore if */
239276
if (debugSession.enabled) {
240-
debugSession('Binding path:', this.getBindingPath());
277+
debugSession('Resolution path:', this.getResolutionPath());
241278
}
242279
}
243280

244281
/**
245282
* Exit the resolution of a binding
246283
*/
247284
popBinding() {
248-
const binding = this.bindings.pop();
285+
const top = this.stack.pop();
286+
if (top === undefined || top.type !== 'binding') {
287+
throw new Error('The top element must be a binding');
288+
}
289+
const binding = top.value;
249290
/* istanbul ignore if */
250291
if (debugSession.enabled) {
251292
debugSession('Exit binding:', binding && binding.toJSON());
252-
debugSession('Binding path:', this.getBindingPath() || '<empty>');
293+
debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
253294
}
254295
return binding;
255296
}
@@ -258,15 +299,40 @@ export class ResolutionSession {
258299
* Get the binding path as `bindingA --> bindingB --> bindingC`.
259300
*/
260301
getBindingPath() {
261-
return this.bindings.map(b => b.key).join(' --> ');
302+
return this.stack
303+
.filter(i => i.type === 'binding')
304+
.map(b => (<Binding>b.value).key)
305+
.join(' --> ');
262306
}
263307

264308
/**
265-
* Get the injection path as `injectionA->injectionB->injectionC`.
309+
* Get the injection path as `injectionA --> injectionB --> injectionC`.
266310
*/
267311
getInjectionPath() {
268-
return this.injections
269-
.map(i => ResolutionSession.describeInjection(i)!.targetName)
312+
return this.stack
313+
.filter(i => i.type === 'injection')
314+
.map(
315+
i =>
316+
ResolutionSession.describeInjection(<Injection>i.value)!.targetName,
317+
)
270318
.join(' --> ');
271319
}
320+
321+
private static describe(e: ResolutionElement) {
322+
switch (e.type) {
323+
case 'injection':
324+
return '@' + ResolutionSession.describeInjection(e.value)!.targetName;
325+
case 'binding':
326+
return e.value.key;
327+
}
328+
}
329+
330+
/**
331+
* Get the resolution path including bindings and injections, for example:
332+
* `bindingA --> @ClassA[0] --> bindingB --> @ClassB.prototype.prop1
333+
* --> bindingC`.
334+
*/
335+
getResolutionPath() {
336+
return this.stack.map(i => ResolutionSession.describe(i)).join(' --> ');
337+
}
272338
}

packages/context/src/resolver.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export function instantiateClass<T>(
5353
debug('Non-injected arguments:', nonInjectedArgs);
5454
}
5555
}
56-
session = session || new ResolutionSession();
5756
const argsOrPromise = resolveInjectedArguments(ctor, '', ctx, session);
5857
const propertiesOrPromise = resolveInjectedProperties(ctor, ctx, session);
5958
let inst: T | Promise<T>;
@@ -201,7 +200,12 @@ export function resolveInjectedArguments(
201200
}
202201
}
203202

204-
const valueOrPromise = resolve(ctx, injection, session);
203+
// Clone the session so that multiple arguments can be resolved in parallel
204+
const valueOrPromise = resolve(
205+
ctx,
206+
injection,
207+
ResolutionSession.fork(session),
208+
);
205209
if (isPromise(valueOrPromise)) {
206210
if (!asyncResolvers) asyncResolvers = [];
207211
asyncResolvers.push(
@@ -314,7 +318,12 @@ export function resolveInjectedProperties(
314318
);
315319
}
316320

317-
const valueOrPromise = resolve(ctx, injection, session);
321+
// Clone the session so that multiple properties can be resolved in parallel
322+
const valueOrPromise = resolve(
323+
ctx,
324+
injection,
325+
ResolutionSession.fork(session),
326+
);
318327
if (isPromise(valueOrPromise)) {
319328
if (!asyncResolvers) asyncResolvers = [];
320329
asyncResolvers.push(valueOrPromise.then(propertyResolver(p)));

packages/context/test/unit/resolver.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ describe('constructor injection', () => {
170170
it('tracks path of bindings', () => {
171171
const context = new Context();
172172
let bindingPath = '';
173+
let resolutionPath = '';
173174

174175
class ZClass {
175176
@inject(
@@ -178,6 +179,7 @@ describe('constructor injection', () => {
178179
// Set up a custom resolve() to access information from the session
179180
(c: Context, injection: Injection, session: ResolutionSession) => {
180181
bindingPath = session.getBindingPath();
182+
resolutionPath = session.getResolutionPath();
181183
},
182184
)
183185
myProp: string;
@@ -196,11 +198,16 @@ describe('constructor injection', () => {
196198
context.bind('z').toClass(ZClass);
197199
context.getSync('x');
198200
expect(bindingPath).to.eql('x --> y --> z');
201+
expect(resolutionPath).to.eql(
202+
'x --> @XClass.constructor[0] --> y --> @YClass.constructor[0]' +
203+
' --> z --> @ZClass.prototype.myProp',
204+
);
199205
});
200206

201207
it('tracks path of bindings for @inject.getter', async () => {
202208
const context = new Context();
203209
let bindingPath = '';
210+
let resolutionPath = '';
204211

205212
class ZClass {
206213
@inject(
@@ -209,6 +216,7 @@ describe('constructor injection', () => {
209216
// Set up a custom resolve() to access information from the session
210217
(c: Context, injection: Injection, session: ResolutionSession) => {
211218
bindingPath = session.getBindingPath();
219+
resolutionPath = session.getResolutionPath();
212220
},
213221
)
214222
myProp: string;
@@ -228,6 +236,10 @@ describe('constructor injection', () => {
228236
const x: XClass = context.getSync('x');
229237
await x.y.z();
230238
expect(bindingPath).to.eql('x --> y --> z');
239+
expect(resolutionPath).to.eql(
240+
'x --> @XClass.constructor[0] --> y --> @YClass.constructor[0]' +
241+
' --> z --> @ZClass.prototype.myProp',
242+
);
231243
});
232244

233245
it('tracks path of injections', () => {

0 commit comments

Comments
 (0)