From d12d8a372a7edab7e48cfd399ad6f91a66daa6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Fri, 10 Jan 2025 01:09:39 +0100 Subject: [PATCH 1/2] refactor(container): add Snapshot model --- .../container/src/snapshot/models/Snapshot.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/container/libraries/container/src/snapshot/models/Snapshot.ts diff --git a/packages/container/libraries/container/src/snapshot/models/Snapshot.ts b/packages/container/libraries/container/src/snapshot/models/Snapshot.ts new file mode 100644 index 0000000..30d2d9b --- /dev/null +++ b/packages/container/libraries/container/src/snapshot/models/Snapshot.ts @@ -0,0 +1,11 @@ +import { + ActivationsService, + BindingService, + DeactivationsService, +} from '@inversifyjs/core'; + +export interface Snapshot { + activationService: ActivationsService; + bindingService: BindingService; + deactivationService: DeactivationsService; +} From a4c91d111e29e88bc3b2378291c068cfcd9e350f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Fri, 10 Jan 2025 01:10:01 +0100 Subject: [PATCH 2/2] feat(container): update Container with snapshot and restore --- .../src/container/services/Container.spec.ts | 70 +++++++++++++++++++ .../src/container/services/Container.ts | 32 ++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) 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 aefd011..f7430f8 100644 --- a/packages/container/libraries/container/src/container/services/Container.spec.ts +++ b/packages/container/libraries/container/src/container/services/Container.spec.ts @@ -50,12 +50,14 @@ describe(Container.name, () => { beforeAll(() => { activationServiceMock = { add: jest.fn(), + clone: jest.fn(), removeAllByModuleId: jest.fn(), removeAllByServiceId: jest.fn(), } as Partial< jest.Mocked > as jest.Mocked; bindingServiceMock = { + clone: jest.fn(), get: jest.fn(), removeAllByModuleId: jest.fn(), removeAllByServiceId: jest.fn(), @@ -63,6 +65,7 @@ describe(Container.name, () => { } as Partial> as jest.Mocked; deactivationServiceMock = { add: jest.fn(), + clone: jest.fn(), removeAllByModuleId: jest.fn(), removeAllByServiceId: jest.fn(), } as Partial< @@ -1009,6 +1012,73 @@ describe(Container.name, () => { }); }); + describe('.restore', () => { + describe('having a container with no snapshots', () => { + let container: Container; + + beforeAll(() => { + container = new Container(); + }); + + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + try { + container.restore(); + } catch (error: unknown) { + result = error; + } + }); + + it('should throw an InversifyContainerError', () => { + const expectedErrorProperties: Partial = { + kind: InversifyContainerErrorKind.invalidOperation, + message: 'No snapshot available to restore', + }; + + expect(result).toBeInstanceOf(InversifyContainerError); + expect(result).toStrictEqual( + expect.objectContaining(expectedErrorProperties), + ); + }); + }); + }); + }); + + describe('.snapshot', () => { + describe('when called', () => { + let result: unknown; + + beforeAll(() => { + result = new Container().snapshot(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should call activationService.clone()', () => { + expect(activationServiceMock.clone).toHaveBeenCalledTimes(1); + expect(activationServiceMock.clone).toHaveBeenCalledWith(); + }); + + it('should call bindingService.clone()', () => { + expect(bindingServiceMock.clone).toHaveBeenCalledTimes(1); + expect(bindingServiceMock.clone).toHaveBeenCalledWith(); + }); + + it('should call deactivationService.clone()', () => { + expect(deactivationServiceMock.clone).toHaveBeenCalledTimes(1); + expect(deactivationServiceMock.clone).toHaveBeenCalledWith(); + }); + + it('should return undefined', () => { + expect(result).toBeUndefined(); + }); + }); + }); + describe('.unbind', () => { let serviceIdentifierFixture: ServiceIdentifier; diff --git a/packages/container/libraries/container/src/container/services/Container.ts b/packages/container/libraries/container/src/container/services/Container.ts index d576bbd..b9c10e7 100644 --- a/packages/container/libraries/container/src/container/services/Container.ts +++ b/packages/container/libraries/container/src/container/services/Container.ts @@ -29,6 +29,7 @@ import { BindToFluentSyntax } from '../../binding/models/BindingFluentSyntax'; import { BindToFluentSyntaxImplementation } from '../../binding/models/BindingFluentSyntaxImplementation'; import { InversifyContainerError } from '../../error/models/InversifyContainerError'; import { InversifyContainerErrorKind } from '../../error/models/InversifyContainerErrorKind'; +import { Snapshot } from '../../snapshot/models/Snapshot'; import { ContainerModule, ContainerModuleLoadOptions, @@ -47,10 +48,11 @@ interface InternalContainerOptions { const DEFAULT_DEFAULT_SCOPE: BindingScope = bindingScopeValues.Transient; export class Container { - readonly #activationService: ActivationsService; - readonly #bindingService: BindingService; - readonly #deactivationService: DeactivationsService; + #activationService: ActivationsService; + #bindingService: BindingService; + #deactivationService: DeactivationsService; readonly #options: InternalContainerOptions; + readonly #snapshots: Snapshot[]; constructor(options?: ContainerOptions) { if (options?.parent !== undefined) { @@ -72,6 +74,7 @@ export class Container { this.#options = { defaultScope: options?.defaultScope ?? DEFAULT_DEFAULT_SCOPE, }; + this.#snapshots = []; } public bind( @@ -233,6 +236,29 @@ export class Container { }); } + public restore(): void { + const snapshot: Snapshot | undefined = this.#snapshots.pop(); + + if (snapshot === undefined) { + throw new InversifyContainerError( + InversifyContainerErrorKind.invalidOperation, + 'No snapshot available to restore', + ); + } + + this.#activationService = snapshot.activationService; + this.#bindingService = snapshot.bindingService; + this.#deactivationService = snapshot.deactivationService; + } + + public snapshot(): void { + this.#snapshots.push({ + activationService: this.#activationService.clone(), + bindingService: this.#bindingService.clone(), + deactivationService: this.#deactivationService.clone(), + }); + } + public async unbind(serviceIdentifier: ServiceIdentifier): Promise { await resolveServiceDeactivations( this.#buildDeactivationParams(),