Skip to content

Commit

Permalink
feat(ioc): resolving child scope from nested container
Browse files Browse the repository at this point in the history
  • Loading branch information
kedrzu committed Jul 23, 2024
1 parent e57690e commit 86038ac
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 8 deletions.
18 changes: 12 additions & 6 deletions packages/ioc/src/Container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Injectable } from './Injectable.js';
import type { Module } from './Module.js';
import { Resolvable } from './Resolvable.js';
import { isResolvable, Resolvable } from './Resolvable.js';

export class Container {
private readonly instances = new Map<symbol, unknown>();
Expand Down Expand Up @@ -84,14 +84,20 @@ export class Container {
return instance;
}

if (injectable instanceof Resolvable) {
if (injectable.scope === 'root' && this.parent) {
instance = this.parent.tryResolve(injectable, scope) as T | undefined;
} else if (injectable.scope === 'child' && !this.parent) {
if (isResolvable(injectable)) {
if (injectable.scope === 'root') {
if (this.parent) {
instance = this.parent.tryResolve(injectable, scope);
} else {
instance = this.doResolve(injectable, scope);
}
} else if (!this.parent) {
// Not possible to resolve a child service in a root container
return undefined;
} else if (this.parent.parent) {
instance = this.parent.get(injectable) ?? this.doResolve(injectable, scope);
} else {
instance = this.doResolve(injectable, scope) as T | undefined;
instance = this.doResolve(injectable, scope);
}

if (instance && injectable.cached) {
Expand Down
4 changes: 4 additions & 0 deletions packages/ioc/src/Resolvable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ export interface ResolvableOptions<T> extends InjectableOptions {
readonly for?: Injectable<T>;
readonly scope?: ResolvableScope;
}

export function isResolvable<T>(injectable: Injectable<T>): injectable is Resolvable<T> {
return injectable instanceof Resolvable;
}
78 changes: 78 additions & 0 deletions packages/ioc/src/Service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,82 @@ describe('child containers', () => {
expect(parent.tryResolve(Service2)).toBeUndefined();
expect(parent.tryResolve(Service3)).toBeUndefined();
});

test('service with child scope with deps in nested container', () => {
let service1Count = 0;
let service2Count = 0;
let service3Count = 0;

const Service1 = defineService({
name: 'service1',
scope: 'root',
setup() {
service1Count++;
return { name: 'service1' };
},
});

const Service2 = defineService({
name: 'service2',
scope: 'child',
setup({ inject }) {
const service1 = inject(Service1);
service2Count++;
expect(service1.name).toBe('service1');
return { name: 'service2' };
},
});

const Service3 = defineService({
name: 'service3',
scope: 'child',
setup({ inject }) {
const service1 = inject(Service1);
const service2 = inject(Service2);
service3Count++;
expect(service1.name).toBe('service1');
expect(service2.name).toBe('service2');
return { name: 'service3' };
},
});

const parent = new Container();
const child = parent.createChild();
const grandchild = child.createChild();

expect(service1Count).toBe(0);
expect(service2Count).toBe(0);
expect(service3Count).toBe(0);

const service2Resolved = child.resolve(Service2);
expect(service2Resolved.name).toBe('service2');
expect(service1Count).toBe(1);
expect(service2Count).toBe(1);
expect(service3Count).toBe(0);

// Should inject from parent
const service3Resolved = grandchild.resolve(Service3);
expect(service3Resolved.name).toBe('service3');
expect(service1Count).toBe(1);
expect(service2Count).toBe(1);
expect(service3Count).toBe(1);

const service1Resolved = grandchild.resolve(Service1);
expect(service1Resolved.name).toBe('service1');
expect(service1Count).toBe(1);
expect(service2Count).toBe(1);
expect(service3Count).toBe(1);

expect(parent.get(Service1)).toBe(service1Resolved);
expect(parent.get(Service2)).toBeUndefined();
expect(parent.get(Service3)).toBeUndefined();

expect(child.get(Service1)).toBe(service1Resolved);
expect(child.get(Service2)).toBe(service2Resolved);
expect(child.get(Service3)).toBeUndefined();

expect(grandchild.get(Service1)).toBe(service1Resolved);
expect(grandchild.get(Service2)).toBe(service2Resolved);
expect(grandchild.get(Service3)).toBe(service3Resolved);
});
});
4 changes: 4 additions & 0 deletions packages/ioc/src/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ export interface ServiceOptions<T, TExtend extends T = T> extends ResolvableOpti
export function defineService<T, TExtend extends T = T>(definition: ServiceOptions<T, TExtend>) {
return new Service<TExtend>(definition as ServiceOptions<TExtend>);
}

export function isService<T>(injectable: Injectable<T>): injectable is Service<T> {
return injectable instanceof Service;
}
9 changes: 7 additions & 2 deletions packages/utils/src/array/mapNotNull.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
export function mapNotNull<T1, T2>(array: Iterable<T1>, map: (item: T1) => T2 | undefined | null) {
export function mapNotNull<T1, T2>(
array: Iterable<T1>,
map: (item: T1, index: number) => T2 | undefined | null,
) {
const result: T2[] = [];

let i = 0;
for (const item of array) {
const mapped = map(item);
const mapped = map(item, i++);
if (mapped != null) {
result.push(mapped);
}
Expand Down

0 comments on commit 86038ac

Please sign in to comment.