На этой странице

Пошаговое руководство · v3

@zirion/ioc

Практическое руководство по @zirion/ioc v3: явное внедрение, transient и request scopes, синхронное и асинхронное разрешение, hooks, примеры и миграция.

Обновлено · ·

Зачем передавать зависимости?

Сервису часто нужны другие объекты, например база данных или отправитель писем. Внедрение зависимостей (сокращённо DI) означает, что эти объекты передают сервису через конструктор, а не создают внутри него. Желающим разобраться глубже в основах предлагаю к ознакомлению множество статей на тему принципов SOLID, где D означает Dependency Inversion, принцип инверсии зависимостей. Внедрение зависимостей помогает реализовать этот принцип.

Рассмотрим простой пример. Здесь BillingService сам создаёт базу данных и инстанс отправителя писем:

class BillingService {
  private database = new PostgresDatabase(process.env.DATABASE_URL!);
  private mailer = new SmtpMailer(process.env.SMTP_URL!);
}

Чтобы проверить этот класс с тестовой базой данных, придётся менять способ создания его зависимостей.

Проще передать готовые объекты в конструктор:

class BillingService {
  constructor(
    private readonly database: Database,
    private readonly mailer: Mailer,
  ) {}
}

Теперь тест может передать подмену базы данных без изменений в BillingService. Но всё ещё нужно решить, где создавать настоящие объекты и как передавать их приложению.

@zirion/ioc помогает собрать эти объекты в общий граф. Вы указываете зависимости в поле inject, а контейнер создаёт значения по мере надобности. Он умеет ждать асинхронные фабрики, легковесен и не имеет зависимостей.

Установка

Выполните команду используя ваш менеджер пакетов:

npm install @zirion/ioc@^3.0.0

Используйте Node.js 20 или новее. Руководство описывает @zirion/ioc@3.0.0. Методы build(), get() и getOrFail() возвращают промисы, поэтому их нужно ожидать через await. Для полностью синхронного графа есть getSync() и getOrFailSync(), возвращающие значения сразу. В пакете есть CommonJS, ES modules и типы TypeScript.

Основные экспорты пакета:

import {
  Container,
  InjectScope,
  DependencyInjectionError,
  ErrorCode,
  type IOnInitialized,
  type IOnFinalized,
  type ILogger,
} from '@zirion/ioc';

Первый контейнер

Зарегистрируйте базу данных и сервис, соберите контейнер, затем запросите сервис:

import { Container, InjectScope } from '@zirion/ioc';

class Database {
  findUser(id: number) {
    return { id, name: 'Ada' };
  }
}

class UserService {
  constructor(private readonly database: Database) {}

  greet(id: number) {
    return `Hello, ${this.database.findUser(id).name}!`;
  }
}

const container = await new Container()
  .add(Database, { scope: InjectScope.SINGLETON })
  .add(UserService, { inject: [Database] })
  .build();

const users = await container.getOrFail(UserService);
console.log(users.greet(1));

Обратите внимание на три вещи:

  1. Зарегистрируйте все зависимости до первого запроса сервиса. Порядок вызовов add() после завершения регистрации не важен.
  2. По умолчанию каждый запрос значения создаёт новый экземпляр: это transient scope. База данных явно зарегистрирована как SINGLETON, поэтому её экземпляр общий.
  3. Дожидайтесь build() и getOrFail() с помощью await.

Термины, которые встретятся дальше

Термин Значение в этом руководстве
Контейнер Хранит правила создания значений и выдаёт их по запросу
Selector Ключ для значения: класс, строка, symbol или объект
Provider Правило создания значения для конкретного selector
Зависимость Значение, нужное другому provider; указывается в inject
Получение значения Запрос значения у контейнера
Scope Правило повторного использования созданного значения
Context Объект, который обозначает один запрос или задачу
Место сборки Часть приложения, где регистрируются все providers

Типы TypeScript не сохраняются при запуске. Эта аннотация помогает редактору, но не сообщает контейнеру, что создавать:

class OrdersService {
  constructor(private readonly repository: OrdersRepository) {}
}

Явно укажите зависимость для контейнера:

container.add(OrdersService, { inject: [OrdersRepository] });

Соберите настройки в одном месте

Разместите регистрации рядом с кодом запуска приложения. Такой файл часто называют местом сборки или composition root. Сами сервисы получают зависимости через конструктор.

import { Container, InjectScope } from '@zirion/ioc';

export const APP_CONFIG = Symbol('APP_CONFIG');

export interface AppConfig {
  databaseUrl: string;
}

export function createContainer(config: AppConfig) {
  return new Container()
    .add(APP_CONFIG, { scope: InjectScope.SINGLETON, valueFactory: () => config })
    .add(Database, { inject: [APP_CONFIG], scope: InjectScope.SINGLETON })
    .add(UserRepository, { inject: [Database] })
    .add(UserService, { inject: [UserRepository] })
    .build();
}

Тогда код запуска остаётся коротким:

const container = await createContainer({
  databaseUrl: process.env.DATABASE_URL!,
});

const users = await container.getOrFail(UserService);

В тесте можно собрать отдельный контейнер с тестовыми фабриками. Сервисам не нужны особые ветки для тестов.

Передавайте полученный сервис обработчику HTTP, воркеру или другой точке входа. Не вызывайте контейнер по всему прикладному коду.

Выберите ключ для каждого значения

add(selector, options?) регистрирует значение. Тот же selector используется в inject, get() и getOrFail().

Класс как ключ

Если selector является классом, контейнер может создать его:

class Clock {
  now() {
    return new Date();
  }
}

const container = new Container().add(Clock);
const clock = await container.getOrFail(Clock); // Clock

Для строк, symbols и объектов нужен valueFactory. Контейнер не знает, как создать их сам.

Строка как ключ

Строки удобны для простых настроек. В большом приложении два модуля могут случайно выбрать одну строку:

const container = new Container()
  .add('REGION', { valueFactory: () => 'eu-central-1' });
const region = await container.getOrFail('REGION');

Symbol как ключ

Symbol помогает избежать такого совпадения. Он подходит для интерфейса TypeScript или настройки:

const MAILER = Symbol('MAILER');

container
  .add(MAILER, { valueFactory: () => new SmtpMailer() })
  .add(NotificationService, { inject: [MAILER] });

Объект как ключ

Ключом может быть и объект. Используйте тот же самый объект, а не новый объект с такими же полями:

const metricsToken = { name: 'metrics' };

const container = new Container().add(metricsToken, {
  scope: InjectScope.SINGLETON,
  valueFactory: () => new MetricsClient(),
});

const metrics = await container.getOrFail(metricsToken);

Для строки, symbol и объекта нужен valueFactory. Один selector нельзя зарегистрировать дважды. В v3.0.0 нельзя заменить регистрацию или привязать несколько providers к одному selector. Пустая строка допустима как selector.

Сохраните типы регистраций TypeScript

add() возвращает тот же контейнер во время выполнения, но расширяет его TypeScript-тип. Сохраняйте результат цепочкой регистраций или присваивайте его новой переменной:

const container = new Container()
  .add('port', { valueFactory: () => 3000 })
  .add('config', {
    scope: InjectScope.SINGLETON,
    valueFactory: async () => ({ region: 'eu-central-1' }),
  });

const port = container.getOrFailSync('port'); // number
const config = await container.getOrFail('config'); // { region: string }

// container.getOrFail('missing'); // TypeScript error

Регистрация отдельной строкой меняет контейнер во время выполнения, но не расширяет тип исходной переменной. После const empty = new Container(); empty.add('port', ...) переменная empty остаётся типизирована как пустой контейнер. Предпочтите цепочку выше; не скрывайте ошибку через any.

Типы учитывают результат фабрики, в том числе значение после await. Класс, возвращённый фабрикой, выводится как экземпляр. При этом типы не проверяют соответствие inject аргументам конструктора и наличие всех зависимостей. Получите каждый главный граф в startup или интеграционном тесте.

Передайте сервисы через конструктор

Порядок selectors в inject должен совпадать с порядком параметров конструктора:

class CheckoutService {
  constructor(
    private readonly inventory: Inventory,
    private readonly payments: Payments,
  ) {}
}

container
  .add(Inventory)
  .add(Payments)
  .add(CheckoutService, {
    inject: [Inventory, Payments],
  });

Порядок и повторы в inject сохраняются. Если singleton указан дважды, в конструктор дважды попадёт один экземпляр; для transient будут созданы два экземпляра. Массив копируется при регистрации: последующие изменения не меняют граф зависимостей.

Зарегистрируйте все selectors до запроса CheckoutService. Если Payments отсутствует, контейнер выбросит DependencyInjectionError с кодом UNKNOWN_TARGET.

Сервис может работать с зависимостями, не зная о контейнере. Не запрашивайте их внутри самого сервиса:

// Не рекомендуется: поиск зависимости скрыт в бизнес-логике.
class CheckoutService {
  constructor(private readonly container: Container<any>) {}

  async checkout() {
    const payments = await this.container.getOrFail(Payments);
  }
}

Передайте Payments в конструктор. get() и getOrFail() вызывайте при запуске приложения или в другой точке входа.

Создавайте значения через фабрики

Используйте valueFactory для настроек, асинхронного запуска и нестандартного создания. Первый аргумент фабрики содержит массив готовых зависимостей. Для request и transient второй аргумент содержит переданный context. Singleton-фабрики не получают context запроса.

const DATABASE = Symbol('DATABASE');
const CONFIG = Symbol('CONFIG');

const container = await new Container()
  .add(CONFIG, {
    scope: InjectScope.SINGLETON,
    valueFactory: () => ({ databaseUrl: process.env.DATABASE_URL! }),
  })
  .add(DATABASE, {
    scope: InjectScope.SINGLETON,
    inject: [CONFIG],
    valueFactory: async ([config]) => {
      const database = new DatabaseClient(config.databaseUrl);
      await database.connect();
      return database;
    },
  })
  .build();

Контейнер дожидается асинхронной фабрики и кеширует результат согласно scope. Singleton сохраняет даже false, 0, '', null, undefined и NaN. Одновременные singleton-разрешения разделяют один выполняющийся вызов фабрики. Request-разрешения делают то же при одном объекте context; после ошибки возможна повторная попытка.

Фабрика может вернуть конструктор класса: контейнер создаст его экземпляр с зависимостями и context. Обычная функция, возвращённая фабрикой, остаётся значением. Обычно проще вернуть готовый объект.

Фабрика пригодится, чтобы:

  • прочитать настройки;
  • подготовить сторонний клиент;
  • дождаться подключения;
  • выбрать реализацию по настройкам;
  • передать тестовую подмену.

Пусть одна фабрика создаёт одно значение. Так проще найти причину ошибки.

Выберите время жизни значения

В v3 три scope. Выбирайте время жизни каждого provider явно и осознанно:

Scope Создание Повторное использование Типичный сценарий
InjectScope.TRANSIENT (по умолчанию) При каждом разрешении или внедрении Не кешируется Сервис без состояния, отдельная операция
InjectScope.SINGLETON При первом разрешении Общий для контейнера Пул БД, конфигурация, общий кеш
InjectScope.REQUEST При первом разрешении с context Общий только для этого объекта context Данные запроса, логгер запроса

Разницу видно на этих регистрациях:

class SharedStore {}
class Operation {}

const container = new Container()
  .add(SharedStore, { scope: InjectScope.SINGLETON })
  .add(Operation);

console.log(
  container.getOrFailSync(SharedStore) === container.getOrFailSync(SharedStore),
); // true
console.log(
  container.getOrFailSync(Operation) === container.getOrFailSync(Operation),
); // false

Для данных запроса укажите scope: InjectScope.REQUEST и передайте context. Transient может зависеть от request-provider, если вызывающий код передал этот context.

В v2 по умолчанию использовался singleton. Перед обновлением регистраций прочитайте гайд v2 → v3.

Не храните данные запроса в singleton

Singleton создаётся один раз и обслуживает все запросы. Значение с InjectScope.REQUEST создаётся отдельно для каждого запроса. Например, RequestState может хранить ID текущего пользователя. Если бы singleton запомнил RequestState Алисы, то при запросе Боба он всё ещё видел бы данные Алисы.

Поэтому значение с InjectScope.REQUEST может использовать singleton, но singleton не может получать такое значение как зависимость. Если общему сервису нужен ID пользователя, передайте его в метод. Если сервис должен хранить стейт только в рамках одного запроса, сделайте его request-scoped.

Прямая зависимость singleton от request вызывает ErrorCode.SINGLETONE_SCOPE_WRONG_CONTEXT. Это точное имя кода в API. Не прячьте request-зависимость за transient внутри singleton: singleton не передаёт context, поэтому request-зависимость завершится ошибкой REQUEST_SCOPE_CONTEXT_REQUIRED. Весь граф singleton должен быть независим от данных запроса.

Один context на запрос

Передавайте объект, обозначающий запрос или задачу. Внутри запроса используйте именно этот объект. Новый объект с такими же полями создаст отдельный request scope.

type RequestContext = {
  requestId: string;
  userId?: string;
};

class RequestState {
  constructor(readonly context: RequestContext) {}
}

const container = await new Container()
  .add(RequestState, { scope: InjectScope.REQUEST })
  .build();

const context: RequestContext = { requestId: 'req-42' };
const first = await container.getOrFail(RequestState, context);
const second = await container.getOrFail(RequestState, context);
const third = await container.getOrFail(RequestState, { requestId: 'req-42' });

console.log(first === second); // true
console.log(first === third);  // false

Класс получает context последним аргументом конструктора. Фабрика получает его вторым аргументом:

const REQUEST_ID = Symbol('REQUEST_ID');

container.add(REQUEST_ID, {
  scope: InjectScope.REQUEST,
  valueFactory: (_dependencies, context?: RequestContext) => context?.requestId,
});

Если запросить request-scoped значение без объекта context, контейнер выбросит REQUEST_SCOPE_CONTEXT_REQUIRED.

Создавайте отдельный context для каждого запроса или задачи. Передавайте его во все верхнеуровневые вызовы get() и getOrFail() этой операции.

Когда создаются providers

Обычно регистрация provider не создаёт значение. Контейнер ждёт, пока вы его запросите.

После создания: onInitialized()

Если у созданного объекта есть onInitialized(), контейнер сразу вызывает этот метод:

class Cache implements IOnInitialized {
  onInitialized() {
    console.log('Cache created');
  }
}

Контейнер дожидается промиса от onInitialized() до завершения get() или getOrFail(). Ошибка hook прерывает разрешение; следующее singleton или request-разрешение может повторить создание. Синхронные методы подходят только для синхронного hook.

При сборке: onFinalized()

build() вызывает finalize(). Метод разрешает singleton-классы с onFinalized() и вызывает hook у объектов-селекторов, которые его реализуют:

class Routes implements IOnFinalized {
  async onFinalized() {
    await this.compileRoutes();
  }

  private async compileRoutes() {}
}

const container = await new Container()
  .add(Routes, { scope: InjectScope.SINGLETON })
  .build();

Singleton-класс с onFinalized() создаётся во время build(). Остальные классы создаются при запросе. Финализация не проверяет все зарегистрированные графы зависимостей.

Request и transient-классы не создаются при финализации. Явно укажите SINGLETON, если класс должен участвовать в запуске приложения; request-hook не выполняется без context.

Вызывайте build() один раз после регистрации. Повторный вызов ещё раз запустит hooks.

Получите сервис

Используйте асинхронный API, если фабрика или hook инициализации может вернуть промис:

const optional = await container.get(UserService);
// UserService | null

const required = await container.getOrFail(UserService);
// UserService или DependencyInjectionError

Используйте get(), если отсутствие значения допустимо. Для обязательного сервиса выбирайте getOrFail(): он выбросит ошибку, если selector не найден.

Оба метода принимают context вторым аргументом:

const handler = await container.getOrFail(RequestHandler, requestContext);

Независимые асинхронные зависимости разрешаются параллельно; аргументы передаются в порядке inject. Если одна фабрика должна дождаться другой, явно задайте эту зависимость в её собственном inject.

Для полностью синхронного графа используйте синхронные методы:

const optional = container.getSync(UserService);
const required = container.getOrFailSync(UserService);

Промис от фабрики, зависимости или onInitialized() вызывает ASYNC_RESOLUTION_REQUIRED. Синхронный вызов может запустить асинхронную фабрику до обнаружения промиса, поэтому не используйте его для проверки синхронности графа. Если граф может быть асинхронным, сразу используйте await. Выполняющийся singleton или request-результат, запущенный синхронным вызовом, доступен последующему асинхронному разрешению.

getOrFail() проверяет регистрацию, а не отсутствие null в результате фабрики. TypeScript запрещает неизвестные selectors во всех четырёх методах. Во время выполнения get() и getSync() возвращают null, если регистрации нет.

Сигнатуры методов, настройки providers и экспортируемые типы находятся в API-справочнике.

Добавьте логгер

Логгер контейнера доступен через container.logger. По умолчанию методы error, warn, info, debug и trace пишут сообщения в консоль.

Можно передать готовый логгер, его класс или фабрику:

class AppLogger implements ILogger {
  error(message: unknown, ...args: unknown[]) {}
  warn(message: unknown, ...args: unknown[]) {}
  info(message: unknown, ...args: unknown[]) {}
  debug(message: unknown, ...args: unknown[]) {}
  trace(message: unknown, ...args: unknown[]) {}
}

const byClass = new Container({ logger: AppLogger });
const byFactory = new Container({ logger: () => new AppLogger() });
const byInstance = new Container({ logger: new AppLogger() });

byClass.logger.info('Container ready');

Контейнер не записывает каждый запрос значения автоматически. Если вашим сервисам нужен логгер, зарегистрируйте и передайте его как обычную зависимость.

Разберитесь с ошибками настройки

При ошибке настройки или получения значения контейнер выбрасывает DependencyInjectionError. Поле code показывает причину. Ошибка может также содержать target и dependency; метод hasCode() проверяет код.

try {
  await container.getOrFail(UserService);
} catch (error) {
  if (
    error instanceof DependencyInjectionError &&
    error.hasCode(ErrorCode.UNKNOWN_TARGET)
  ) {
    console.error('Composition root собран не полностью');
  }
  throw error;
}
Код Значение Причина
UNKNOWN_SCOPE 1 Указан неподдерживаемый scope
UNKNOWN_TARGET 2 Обязательный selector не зарегистрирован
TARGET_TYPE_BAD_RESOLVER 3 Selector требует фабрику, но её нет
TARGET_NULL 4 Selector равен null или undefined
TARGET_DUPLICATE 5 Selector уже зарегистрирован
REQUEST_SCOPE_CONTEXT_REQUIRED 6 У request-scoped значения нет объекта context
SINGLETONE_SCOPE_WRONG_CONTEXT 7 Singleton прямо зависит от request-scoped значения
CIRCULAR_DEPENDENCY 8 В графе зависимостей есть цикл
ASYNC_RESOLUTION_REQUIRED 9 Синхронный вызов встретил асинхронную работу

Обычно эти ошибки говорят о неправильной настройке контейнера. Ищите их при запуске или в небольшом тесте контейнера, а не скрывайте внутри бизнес-кода.

Разложите код приложения по файлам

Храните настройку контейнера в одном файле, а прикладной код отдельно:

src/
├── application/
│   └── user-service.ts
├── domain/
│   └── user.ts
├── infrastructure/
│   ├── postgres-user-repository.ts
│   └── console-logger.ts
├── interfaces/
│   └── http-server.ts
└── container.ts       # composition root

Простое правило для каждой части:

  1. Передавайте зависимости прикладным классам через конструктор.
  2. Держите код базы данных, сети и других внешних систем в отдельных модулях.
  3. Регистрируйте конкретные классы и selectors в container.ts.
  4. При запуске запрашивайте у контейнера только сервер, воркер или другую точку входа.
  5. Для каждого запроса создавайте context и получайте request-scoped сервис.

У интерфейса нет значения во время выполнения. Используйте symbol как его selector:

export interface UserRepository {
  findById(id: string): Promise<User | null>;
}

export const USER_REPOSITORY = Symbol('USER_REPOSITORY');

Затем свяжите symbol с настоящей реализацией при настройке:

container
  .add(USER_REPOSITORY, {
    valueFactory: () => new PostgresUserRepository(),
  })
  .add(UserService, {
    inject: [USER_REPOSITORY],
  });

Пример: обработайте HTTP-запрос

Полный пример для Fetch API создаёт один context на запрос. Общий сервис явно зарегистрирован как singleton; логгер и обработчик принадлежат запросу:

import { Container, InjectScope } from '@zirion/ioc';

type HttpContext = {
  requestId: string;
  request: Request;
};

class UserService {
  async list() {
    return [{ id: 1, name: 'Ada' }];
  }
}

class RequestLogger {
  constructor(readonly context: HttpContext) {}

  info(message: string) {
    console.info(`[${this.context.requestId}] ${message}`);
  }
}

class RequestHandler {
  constructor(
    private readonly users: UserService,
    private readonly logger: RequestLogger,
    readonly context: HttpContext,
  ) {}

  async handle() {
    this.logger.info(`Handling ${this.context.request.method}`);
    return Response.json(await this.users.list());
  }
}

const container = new Container()
  .add(UserService, { scope: InjectScope.SINGLETON })
  .add(RequestLogger, { scope: InjectScope.REQUEST })
  .add(RequestHandler, {
    inject: [UserService, RequestLogger],
    scope: InjectScope.REQUEST,
  });

export async function handleRequest(request: Request) {
  const context: HttpContext = {
    request,
    requestId: crypto.randomUUID(),
  };

  const handler = await container.getOrFail(RequestHandler, context);
  return handler.handle();
}

UserService общий для контейнера. Повторные разрешения RequestLogger и RequestHandler с тем же context возвращают экземпляры этого запроса. Другой context получает независимые экземпляры. Когда context больше никому не нужен, связанные значения могут быть удалены сборщиком мусора. Приложение по-прежнему отвечает за внешние ресурсы.

Полный сервер с node:http находится в запускаемых примерах.

Пример: внедрите функцию как значение

Фабрика может вернуть обычную функцию: контейнер не вызывает её автоматически. Это удобно для callbacks, форматирования и адаптеров:

import { Container, InjectScope } from '@zirion/ioc';

const container = new Container().add('formatPrice', {
  scope: InjectScope.SINGLETON,
  valueFactory: () => (amount: number) => `$${amount.toFixed(2)}`,
});

const formatPrice = container.getOrFailSync('formatPrice');
console.log(formatPrice(12.5)); // $12.50

Тип formatPrice выводится как (amount: number) => string. Фабрика кешируется как singleton; вызов форматирования относится к обычному коду приложения.

Пример: разделите асинхронную инициализацию

Два потребителя могут запросить singleton, пока его фабрика ещё выполняется. Оба получат один результат; фабрика запустится один раз:

import { Container, InjectScope } from '@zirion/ioc';

let attempts = 0;

const container = new Container().add('config', {
  scope: InjectScope.SINGLETON,
  valueFactory: async () => {
    attempts += 1;
    return { region: 'eu-central-1' };
  },
});

const [first, second] = await Promise.all([
  container.getOrFail('config'),
  container.getOrFail('config'),
]);

console.log(first === second); // true
console.log(attempts); // 1

Если фабрика отклонит промис, оба потребителя получат ошибку. Следующее разрешение повторит создание. Не помещайте неповторяемые операции в инициализацию без возможности восстановиться после неудачной попытки.

Пример: дождитесь hook инициализации

Экземпляр доступен только после завершения асинхронного hook:

import { Container, InjectScope, type IOnInitialized } from '@zirion/ioc';

class Catalog implements IOnInitialized {
  items: string[] = [];

  async onInitialized() {
    this.items = await Promise.resolve(['book', 'pen']);
  }
}

const container = new Container()
  .add(Catalog, { scope: InjectScope.SINGLETON });

const catalog = await container.getOrFail(Catalog);
console.log(catalog.items); // ['book', 'pen']

Это же правило действует для объектов с onInitialized(), возвращённых фабрикой. getOrFailSync(Catalog) до завершения инициализации бросит ASYNC_RESOLUTION_REQUIRED; для такого provider используйте асинхронный API.

Проверьте сервисы и настройку

Для большинства unit-тестов создайте сервис напрямую и передайте подмену зависимости. Контейнер не нужен:

const repository: UserRepository = {
  async findById(id) {
    return { id, name: 'Test user' };
  },
};

const service = new UserService(repository);

Отдельным тестом проверьте, что регистрации контейнера согласованы:

import { expect, it } from 'vitest';

it('builds the user graph', async () => {
  const container = await new Container()
    .add(USER_REPOSITORY, {
      valueFactory: () => ({
        findById: async (id: string) => ({ id, name: 'Test user' }),
      }),
    })
    .add(UserService, { inject: [USER_REPOSITORY] })
    .build();

  await expect(container.getOrFail(UserService)).resolves.toBeInstanceOf(UserService);
});

Создавайте новый контейнер для каждого теста. В v3.0.0 нельзя заменить регистрацию, а общий контейнер может сохранить singleton из другого теста.

Для request scope проверьте, что один context возвращает один объект, а другой context даёт другой объект:

const requestA = {};
const requestB = {};

expect(await container.getOrFail(RequestState, requestA))
  .toBe(await container.getOrFail(RequestState, requestA));
expect(await container.getOrFail(RequestState, requestA))
  .not.toBe(await container.getOrFail(RequestState, requestB));

Гайды по миграции

Частые проблемы

get() возвращает null

Selector не зарегистрирован. Проверьте, что в add() и get() используется тот же класс, строка, symbol или объект. Для обязательного значения используйте getOrFail().

UNKNOWN_TARGET при разрешении зарегистрированного класса

В inject этого класса отсутствует один из selectors. Зарегистрируйте его до получения сервиса и проверьте, что модули импортируют один и тот же symbol.

TARGET_TYPE_BAD_RESOLVER

Строке, symbol или объекту нужен корректный valueFactory.

REQUEST_SCOPE_CONTEXT_REQUIRED

Передайте context вторым аргументом и используйте этот же объект в течение всего запроса:

await container.getOrFail(RequestHandler, requestContext);

SINGLETONE_SCOPE_WRONG_CONTEXT

Singleton зависит от request-scoped provider. Сделайте родительский provider request-scoped, передайте данные запроса в метод или внедрите фабрику, принимающую context.

Значение создаётся во время build()

У singleton-класса есть onFinalized(): build() разрешает этот класс для startup hook. Transient-класс с таким же hook пропускается.

Значение создаётся снова

В v3 по умолчанию используется TRANSIENT. Укажите scope: InjectScope.SINGLETON, если повторные разрешения должны возвращать одно значение. Если фабрика или hook явного singleton завершилась ошибкой, следующее разрешение повторит создание.

CIRCULAR_DEPENDENCY

Два или больше providers зависят друг от друга через inject. Вынесите общую логику в третий сервис или разделите настройку и выполнение. Фабрика, самостоятельно обращающаяся к контейнеру, скрывает зависимости от проверки графа; предпочтите явный inject.

ASYNC_RESOLUTION_REQUIRED

Синхронный метод получил промис. Используйте await container.getOrFail(...) или сделайте все фабрики и onInitialized() этого графа синхронными. Уже созданный синхронный singleton можно получить синхронно, но не стоит полагаться на порядок запуска, чтобы скрыть асинхронность графа.

Ресурсы остаются открытыми при завершении приложения

У контейнера нет шага завершения. Закрывайте серверы, пулы БД, очереди и другие долгоживущие ресурсы в обработчике завершения приложения.


Это руководство описывает @zirion/ioc v3.0.0. Детали реализации есть в репозитории, история версий на странице проекта.

Наверх