From ac22bb9617fec78a95bfe4b527d8bc145cda437a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Sat, 11 Jan 2025 14:16:45 +0100 Subject: [PATCH 1/3] refactor(core): update OneToManyMapStar with getAllKeys --- .../common/models/OneToManyMapStar.spec.ts | 40 +++++++++++++++++++ .../src/common/models/OneToManyMapStar.ts | 6 +++ .../models/__mocks__/OneToManyMapStar.ts | 7 ++++ 3 files changed, 53 insertions(+) diff --git a/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts b/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts index cb4e92e0..3f542d9b 100644 --- a/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts +++ b/packages/container/libraries/core/src/common/models/OneToManyMapStar.spec.ts @@ -127,6 +127,46 @@ describe(OneToManyMapStar.name, () => { }); }); + describe('.getAllKeys', () => { + describe('having a OneToManyMapStart with a single model', () => { + let modelFixture: unknown; + let relationFixture: Required; + let relationKeyFixture: RelationKey.foo; + let oneToManyMapStar: OneToManyMapStar; + + beforeAll(() => { + modelFixture = Symbol(); + relationFixture = { + bar: 3, + foo: 'foo', + }; + relationKeyFixture = RelationKey.foo; + oneToManyMapStar = new OneToManyMapStar({ + bar: { + isOptional: true, + }, + foo: { + isOptional: false, + }, + }); + + oneToManyMapStar.set(modelFixture, relationFixture); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = [...oneToManyMapStar.getAllKeys(relationKeyFixture)]; + }); + + it('should return expected result', () => { + expect(result).toStrictEqual([relationFixture[relationKeyFixture]]); + }); + }); + }); + }); + describe('.removeByRelation', () => { describe('having a OneToManyMapStart with a no models', () => { let relationFixture: Required; diff --git a/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts b/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts index 61726b77..d723a695 100644 --- a/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts +++ b/packages/container/libraries/core/src/common/models/OneToManyMapStar.ts @@ -63,6 +63,12 @@ export class OneToManyMapStar return this.#relationToModelsMaps[key].get(value)?.values(); } + public getAllKeys( + key: TKey, + ): Iterable { + return this.#relationToModelsMaps[key].keys(); + } + public removeByRelation( key: TKey, value: Required[TKey], diff --git a/packages/container/libraries/core/src/common/models/__mocks__/OneToManyMapStar.ts b/packages/container/libraries/core/src/common/models/__mocks__/OneToManyMapStar.ts index 80670a8c..0f39f81b 100644 --- a/packages/container/libraries/core/src/common/models/__mocks__/OneToManyMapStar.ts +++ b/packages/container/libraries/core/src/common/models/__mocks__/OneToManyMapStar.ts @@ -5,6 +5,8 @@ const cloneMock: jest.Mock = jest.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const getMock: jest.Mock = jest.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any +const getAllKeysMock: jest.Mock = jest.fn(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any const removeByRelationMock: jest.Mock = jest.fn(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const setMock: jest.Mock = jest.fn(); @@ -16,6 +18,10 @@ export class OneToManyMapStar { value: Required[TKey], ) => Iterable | undefined; + public readonly getAllKeys: ( + key: TKey, + ) => Iterable; + public readonly removeByRelation: ( key: TKey, value: Required[TKey], @@ -26,6 +32,7 @@ export class OneToManyMapStar { constructor() { this.clone = cloneMock; this.get = getMock; + this.getAllKeys = getAllKeysMock; this.removeByRelation = removeByRelationMock; this.set = setMock; } From f487c1bef8ef07d22f693b9d1feaf012fd35c909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Sat, 11 Jan 2025 14:17:11 +0100 Subject: [PATCH 2/3] feat(core): update BindingService with getNonParentBoundServices --- .changeset/dull-books-cover.md | 5 ++++ .../binding/services/BindingService.spec.ts | 29 +++++++++++++++++++ .../src/binding/services/BindingService.ts | 4 +++ 3 files changed, 38 insertions(+) create mode 100644 .changeset/dull-books-cover.md diff --git a/.changeset/dull-books-cover.md b/.changeset/dull-books-cover.md new file mode 100644 index 00000000..68bc44a3 --- /dev/null +++ b/.changeset/dull-books-cover.md @@ -0,0 +1,5 @@ +--- +"@inversifyjs/core": minor +--- + +Updated `BindingService` with `getNonParentBoundServices` diff --git a/packages/container/libraries/core/src/binding/services/BindingService.spec.ts b/packages/container/libraries/core/src/binding/services/BindingService.spec.ts index ba15c74a..49023f4c 100644 --- a/packages/container/libraries/core/src/binding/services/BindingService.spec.ts +++ b/packages/container/libraries/core/src/binding/services/BindingService.spec.ts @@ -209,6 +209,35 @@ describe(BindingService.name, () => { }); }); + describe('.getNonParentBoundServices', () => { + describe('when called', () => { + let serviceIdsFixture: ServiceIdentifier[]; + + let result: Iterable; + + beforeAll(() => { + serviceIdsFixture = ['service-id-1', 'service-id-2']; + + bindingMapsMock.getAllKeys.mockReturnValueOnce(serviceIdsFixture); + + result = bindingService.getNonParentBoundServices(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should return the non-parent bound services', () => { + expect(result).toStrictEqual(serviceIdsFixture); + }); + + it('should call bindingMaps.getAllKeys()', () => { + expect(bindingMapsMock.getAllKeys).toHaveBeenCalledTimes(1); + expect(bindingMapsMock.getAllKeys).toHaveBeenCalledWith('serviceId'); + }); + }); + }); + describe('.removeAllByModuleId', () => { let moduleIdFixture: number; diff --git a/packages/container/libraries/core/src/binding/services/BindingService.ts b/packages/container/libraries/core/src/binding/services/BindingService.ts index cc844365..642bf2ca 100644 --- a/packages/container/libraries/core/src/binding/services/BindingService.ts +++ b/packages/container/libraries/core/src/binding/services/BindingService.ts @@ -61,6 +61,10 @@ export class BindingService implements Cloneable { ); } + public getNonParentBoundServices(): Iterable { + return this.#bindingMaps.getAllKeys(BindingRelationKind.serviceId); + } + public getByModuleId( moduleId: number, ): Iterable> | undefined { From acfe7ec1fe987ef94485e86fad3b5d9b426acf4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Sat, 11 Jan 2025 14:17:27 +0100 Subject: [PATCH 3/3] feat(core): update Container with unbindAll --- .changeset/fluffy-ravens-heal.md | 5 ++ .../src/container/services/Container.spec.ts | 70 +++++++++++++++++++ .../src/container/services/Container.ts | 30 ++++++++ 3 files changed, 105 insertions(+) create mode 100644 .changeset/fluffy-ravens-heal.md diff --git a/.changeset/fluffy-ravens-heal.md b/.changeset/fluffy-ravens-heal.md new file mode 100644 index 00000000..c5227441 --- /dev/null +++ b/.changeset/fluffy-ravens-heal.md @@ -0,0 +1,5 @@ +--- +"@inversifyjs/container": minor +--- + +Updated `Container` with `unbindAll` diff --git a/packages/container/libraries/container/src/container/services/Container.spec.ts b/packages/container/libraries/container/src/container/services/Container.spec.ts index f7430f8c..08edabc3 100644 --- a/packages/container/libraries/container/src/container/services/Container.spec.ts +++ b/packages/container/libraries/container/src/container/services/Container.spec.ts @@ -59,6 +59,7 @@ describe(Container.name, () => { bindingServiceMock = { clone: jest.fn(), get: jest.fn(), + getNonParentBoundServices: jest.fn(), removeAllByModuleId: jest.fn(), removeAllByServiceId: jest.fn(), set: jest.fn(), @@ -1153,6 +1154,75 @@ describe(Container.name, () => { }); }); + describe('.unbindAll', () => { + describe('when called', () => { + let serviceIdsFixture: string[]; + let result: unknown; + + beforeAll(async () => { + serviceIdsFixture = ['service1', 'service2']; + bindingServiceMock.getNonParentBoundServices.mockReturnValueOnce( + serviceIdsFixture, + ); + + result = await new Container().unbindAll(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call resolveServiceDeactivations for each service', () => { + expect(resolveServiceDeactivations).toHaveBeenCalledTimes( + serviceIdsFixture.length, + ); + for (const serviceId of serviceIdsFixture) { + expect(resolveServiceDeactivations).toHaveBeenCalledWith( + expect.any(Object), + serviceId, + ); + } + }); + + it('should call removeAllByServiceId on activationService for each service', () => { + expect( + activationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledTimes(serviceIdsFixture.length); + for (const serviceId of serviceIdsFixture) { + expect( + activationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledWith(serviceId); + } + }); + + it('should call removeAllByServiceId on bindingService for each service', () => { + expect(bindingServiceMock.removeAllByServiceId).toHaveBeenCalledTimes( + serviceIdsFixture.length, + ); + for (const serviceId of serviceIdsFixture) { + expect(bindingServiceMock.removeAllByServiceId).toHaveBeenCalledWith( + serviceId, + ); + } + }); + + it('should call removeAllByServiceId on deactivationService for each service', () => { + expect( + deactivationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledTimes(serviceIdsFixture.length); + for (const serviceId of serviceIdsFixture) { + expect( + deactivationServiceMock.removeAllByServiceId, + ).toHaveBeenCalledWith(serviceId); + } + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + describe('.unload', () => { let containerModuleFixture: ContainerModule; diff --git a/packages/container/libraries/container/src/container/services/Container.ts b/packages/container/libraries/container/src/container/services/Container.ts index b9c10e7a..a711a151 100644 --- a/packages/container/libraries/container/src/container/services/Container.ts +++ b/packages/container/libraries/container/src/container/services/Container.ts @@ -270,6 +270,36 @@ export class Container { this.#deactivationService.removeAllByServiceId(serviceIdentifier); } + public async unbindAll(): Promise { + const deactivationParams: DeactivationParams = + this.#buildDeactivationParams(); + + const nonParentBoundServiceIds: ServiceIdentifier[] = [ + ...this.#bindingService.getNonParentBoundServices(), + ]; + + await Promise.all( + nonParentBoundServiceIds.map( + async (serviceId: ServiceIdentifier): Promise => + resolveServiceDeactivations(deactivationParams, serviceId), + ), + ); + + /* + * Removing service related objects here so unload is deterministic. + * + * Removing service related objects as soon as resolveModuleDeactivations takes + * effect leads to module deactivations not triggering previously deleted + * deactivations, introducing non determinism depending in the order in which + * services are deactivated. + */ + for (const serviceId of nonParentBoundServiceIds) { + this.#activationService.removeAllByServiceId(serviceId); + this.#bindingService.removeAllByServiceId(serviceId); + this.#deactivationService.removeAllByServiceId(serviceId); + } + } + public async unload(...modules: ContainerModule[]): Promise { const deactivationParams: DeactivationParams = this.#buildDeactivationParams();