Skip to content

Commit

Permalink
fix: #38 added new container type - "scope"
Browse files Browse the repository at this point in the history
  • Loading branch information
mnasyrov committed May 26, 2024
1 parent e9c66f5 commit c6ce6fe
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 12 deletions.
23 changes: 22 additions & 1 deletion packages/ditox/src/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
PARENT_CONTAINER,
ResolverError,
} from './container';
import {injectable} from './utils';
import {optional, token} from './tokens';
import {injectable} from './utils';

const NUMBER = token<number>('number');
const STRING = token<string>('string');
Expand Down Expand Up @@ -160,6 +160,27 @@ describe('Container', () => {
expect(factory).toBeCalledTimes(1);
});

it('should bind once a factory with "scoped" scope', async () => {
let counter = 0;
const factory = jest.fn(() => ++counter);

const root = createContainer();
root.bindFactory(NUMBER, factory);
expect(root.get(NUMBER)).toBe(1);
expect(factory).toBeCalledTimes(1);

const scopeContainer = createContainer(root, {type: 'scope'});
scopeContainer.bindFactory(NUMBER, factory, {scope: 'scoped'});
factory.mockClear();
expect(scopeContainer.get(NUMBER)).toBe(2);
expect(factory).toBeCalledTimes(1);

const child = createContainer(scopeContainer);
factory.mockClear();
expect(child.get(NUMBER)).toBe(2);
expect(factory).toBeCalledTimes(0);
});

test('order of scoped containers', () => {
const parent = createContainer();
const container1 = createContainer(parent);
Expand Down
53 changes: 42 additions & 11 deletions packages/ditox/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ export type FactoryOptions<T> =
scope: 'transient';
};

export type ContainerType = 'scope';

export type ContainerOptions = {
type?: ContainerType;
};

/**
* Dependency container.
*/
export type Container = {
readonly type?: ContainerType;

/**
* Binds a value for the token
*/
Expand Down Expand Up @@ -115,7 +123,11 @@ export type FactoriesMap = Map<symbol, FactoryContext<any>>;
export const FACTORIES_MAP: Token<FactoriesMap> = token('ditox.FactoriesMap');

/** @internal */
type Resolver = <T>(token: Token<T>, origin: Container) => T | typeof NOT_FOUND;
type Resolver = <T>(
token: Token<T>,
origin: Container,
scopeRoot?: Container,
) => T | typeof NOT_FOUND;

/** @internal */
function getScope<T>(options?: FactoryOptions<T>): FactoryScope {
Expand Down Expand Up @@ -147,11 +159,16 @@ function isInternalToken<T>(token: Token<T>): boolean {
*
* @param parentContainer - Optional parent container.
*/
export function createContainer(parentContainer?: Container): Container {
export function createContainer(
parentContainer?: Container,
options?: ContainerOptions,
): Container {
const values: ValuesMap = new Map<symbol, any>();
const factories: FactoriesMap = new Map<symbol, FactoryContext<any>>();

const container: Container = {
type: options?.type,

bindValue<T>(token: Token<T>, value: T): void {
if (isInternalToken(token)) {
return;
Expand Down Expand Up @@ -238,6 +255,7 @@ export function createContainer(parentContainer?: Container): Container {
function resolver<T>(
token: Token<T>,
origin: Container,
scopeRoot?: Container,
): T | typeof NOT_FOUND {
const value = values.get(token.symbol);
const hasValue = value !== undefined || values.has(token.symbol);
Expand Down Expand Up @@ -265,16 +283,26 @@ export function createContainer(parentContainer?: Container): Container {
}

case 'scoped': {
// Create a value within the origin container and cache it.
const value = factoryContext.factory(origin);
origin.bindValue(token, value);
const scope = scopeRoot
? scopeRoot
: container.type === 'scope'
? container
: origin;

if (origin !== container) {
// Bind a fake factory with actual options to make onRemoved() works.
origin.bindFactory(token, FAKE_FACTORY, factoryContext.options);
}
if (container === scope && hasValue) {
return value;
} else {
// Create a value within the scope container and cache it.
const value = factoryContext.factory(scope);
scope.bindValue(token, value);

if (scope !== container) {
// Bind a fake factory with actual options to make onRemoved() works.
scope.bindFactory(token, FAKE_FACTORY, factoryContext.options);
}

return value;
return value;
}
}

case 'transient': {
Expand All @@ -290,7 +318,10 @@ export function createContainer(parentContainer?: Container): Container {

const parentResolver = parentContainer?.get(RESOLVER);
if (parentResolver) {
return parentResolver(token, origin);
const bubbledScopeRoot =
!scopeRoot && container.type === 'scope' ? container : scopeRoot;

return parentResolver(token, origin, bubbledScopeRoot);
}

return NOT_FOUND;
Expand Down

0 comments on commit c6ce6fe

Please sign in to comment.