From 86038ac8534ff75d417125f371f36599a03a83c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99drzy=C5=84ski?= Date: Tue, 23 Jul 2024 08:09:38 +0200 Subject: [PATCH] feat(ioc): resolving child scope from nested container --- packages/ioc/src/Container.ts | 18 ++++-- packages/ioc/src/Resolvable.ts | 4 ++ packages/ioc/src/Service.test.ts | 78 ++++++++++++++++++++++++++ packages/ioc/src/Service.ts | 4 ++ packages/utils/src/array/mapNotNull.ts | 9 ++- 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/packages/ioc/src/Container.ts b/packages/ioc/src/Container.ts index 5448d12..8195757 100644 --- a/packages/ioc/src/Container.ts +++ b/packages/ioc/src/Container.ts @@ -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(); @@ -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) { diff --git a/packages/ioc/src/Resolvable.ts b/packages/ioc/src/Resolvable.ts index fd8e6a7..5d35f55 100644 --- a/packages/ioc/src/Resolvable.ts +++ b/packages/ioc/src/Resolvable.ts @@ -30,3 +30,7 @@ export interface ResolvableOptions extends InjectableOptions { readonly for?: Injectable; readonly scope?: ResolvableScope; } + +export function isResolvable(injectable: Injectable): injectable is Resolvable { + return injectable instanceof Resolvable; +} diff --git a/packages/ioc/src/Service.test.ts b/packages/ioc/src/Service.test.ts index d808b13..929d2af 100644 --- a/packages/ioc/src/Service.test.ts +++ b/packages/ioc/src/Service.test.ts @@ -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); + }); }); diff --git a/packages/ioc/src/Service.ts b/packages/ioc/src/Service.ts index c9d9f26..d3622dc 100644 --- a/packages/ioc/src/Service.ts +++ b/packages/ioc/src/Service.ts @@ -32,3 +32,7 @@ export interface ServiceOptions extends ResolvableOpti export function defineService(definition: ServiceOptions) { return new Service(definition as ServiceOptions); } + +export function isService(injectable: Injectable): injectable is Service { + return injectable instanceof Service; +} diff --git a/packages/utils/src/array/mapNotNull.ts b/packages/utils/src/array/mapNotNull.ts index 0fa280b..8575b3d 100644 --- a/packages/utils/src/array/mapNotNull.ts +++ b/packages/utils/src/array/mapNotNull.ts @@ -1,7 +1,12 @@ -export function mapNotNull(array: Iterable, map: (item: T1) => T2 | undefined | null) { +export function mapNotNull( + array: Iterable, + 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); }