e22m4u 3 дней назад
Родитель
Сommit
af2b3ecccf

+ 4 - 0
.mocharc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  extension: ['js'],
+  spec: 'src/**/*.spec.js',
+}

+ 0 - 4
.mocharc.json

@@ -1,4 +0,0 @@
-{
-  "extension": ["js"],
-  "spec": "src/**/*.spec.js"
-}

+ 328 - 56
README.md

@@ -8,6 +8,7 @@
 ## Содержание
 
 - [Установка](#Установка)
+- [Мотивация](#Мотивация)
 - [Описание](#Описание)
 - [Базовые примеры](#Базовые-примеры)
 - [ServiceContainer](#ServiceContainer)
@@ -37,6 +38,157 @@ import {Service} from '@e22m4u/js-service';
 const {Service} = require('@e22m4u/js-service');
 ```
 
+## Мотивация
+
+При росте проекта, построенного на классах, разработчики часто сталкиваются
+с проблемой управления зависимостями. Классы начинают зависеть друг от друга,
+и для их совместной работы требуется где-то создавать экземпляры и передавать
+их в конструкторы или методы других классов. Ручное внедрение зависимостей
+быстро приводит к сложному и запутанному коду.
+
+#### Проблема: Ручное управление зависимостями
+
+Представим типичную ситуацию - есть сервис для работы с пользователями
+`UserService`, который нуждается в логгере `Logger` и клиенте для доступа
+к базе данных `DatabaseClient`.
+
+Без библиотеки это может выглядеть так:
+
+```js
+// зависимости
+class Logger {
+  log(message) {
+    console.log(message);
+  }
+}
+
+class DatabaseClient {
+  query(id) {
+    return {id, name: 'John Doe'};
+  }
+}
+
+// сервис, который зависит от двух других классов
+class UserService {
+  constructor(db, logger) {
+    if (!db || !logger) {
+      throw new Error('Database and Logger are required!');
+    }
+    this.db = db;
+    this.logger = logger;
+  }
+
+  findUser(id) {
+    this.logger.log(`Searching for user with id: ${id}`);
+    return this.db.query(id);
+  }
+}
+
+// -- точка входа в приложение (или где-то в коде) ---
+
+// нужно вручную создать все зависимости
+const logger = new Logger();
+const dbClient = new DatabaseClient();
+
+// нужно вручную передать их в конструктор
+const userService = new UserService(dbClient, logger);
+
+userService.findUser(123);
+```
+
+Недостатки ручного управления зависимостями:
+
+- Жесткая связь и сложность.  
+  Точка входа в приложение превращается в "фабрику", которая знает,
+  как создавать и связывать все компоненты. Если `DatabaseClient` тоже
+  начнет зависеть от `Logger`, порядок создания усложнится.
+
+- Проблемы с тестированием.  
+  Чтобы протестировать `UserService` в изоляции, нужно создать "моки"
+  для `Logger` и `DatabaseClient` и вручную передать их в конструктор.
+  Это громоздко и требует дополнительного кода в каждом тесте.
+
+#### Решение: Инверсия управления
+
+Эта библиотека была создана для решения именно этих проблем. Она реализует
+принцип *инверсии управления (IoC)*, при котором контроль над созданием
+и предоставлением зависимостей передается от компонента к специализированному
+контейнеру.
+
+С библиотекой код становится проще:
+
+```js
+import {Service} from '@e22m4u/js-service';
+
+// зависимости (остаются простыми классами)
+class Logger {
+  log(message) {
+    console.log(message);
+  }
+}
+
+class DatabaseClient {
+  query(id) {
+    return {id, name: 'John Doe'};
+  }
+}
+
+// сервис наследует `Service` для доступа к `getService`
+class UserService extends Service {
+  findUser(id) {
+    // зависимости запрашиваются "по требованию" где они нужны,
+    // контейнер сам позаботится об их создании и кешировании
+    const logger = this.getService(Logger);
+    const db = this.getService(DatabaseClient);
+    
+    logger.log(`Searching for user with id: ${id}`);
+    return db.query(id);
+  }
+}
+
+// -- точка входа в приложение ---
+
+// мы просто создаем экземпляр нужного сервиса
+const userService = new UserService();
+userService.findUser(123);
+```
+
+Преимущества этого подхода:
+
+- Слабая связанность.  
+  `UserService` больше не знает, как создавать `Logger` или `DatabaseClient`.
+  Он просто заявляет о своей потребности в них, вызывая `this.getService()`.
+
+- Простота и чистота кода.  
+  Точка входа в приложение становится тривиальной. Логика сборки зависимостей
+  полностью инкапсулирована внутри контейнера, который неявно управляется
+  базовым классом `Service`
+
+- Легкость тестирования.  
+  Подменить зависимость стало невероятно просто. Метод `setService` позволяет
+  "на лету" подставить мок-объект.
+
+Пример подмены зависимости (mocking):
+
+```js
+// в файле теста
+const userService = new UserService();
+
+// создание мок-логгера
+const mockLogger = {log: () => {}}; // не будет писать в консоль
+
+// подмена реализации Logger в контейнере этого сервиса
+userService.setService(Logger, mockLogger);
+
+// теперь при вызове findUser будет использован наш мок-объект
+userService.findUser(456);
+```
+
+Библиотека избавляет от рутины ручного управления зависимостями, делая
+архитектуру проекта гибкой, масштабируемой и легко тестируемой. Разработчик
+может сосредоточиться на бизнес-логике, а не на том, как и в каком порядке
+создавать и связывать объекты.
+
 ## Описание
 
 Модуль экспортирует два основных класса, `ServiceContainer` и `Service`,
@@ -188,12 +340,21 @@ const myService = container.get(MyService);
 - [`add(ctor, ...args)`](#servicecontaineradd) добавить конструктор в контейнер (ленивая инициализация);
 - [`use(ctor, ...args)`](#servicecontaineruse) добавить конструктор и сразу создать экземпляр;
 - [`set(ctor, service)`](#servicecontainerset) добавить конструктор и связанный с ним готовый экземпляр;
-- [`find(predicate, [noParent])`](#servicecontainerfind) найти сервис удовлетворяющий условию;
+- [`find(predicate, noParent = false)`](#servicecontainerfind) найти сервис удовлетворяющий условию;
 - [`getParent()`](#servicecontainergetparent-и-servicecontainerhasparent) получить родительский сервис-контейнер;
 - [`hasParent()`](#servicecontainergetparent-и-servicecontainerhasparent) проверить наличие родительского сервис-контейнера;
 
 В сигнатурах методов используется вспомогательный тип конструктора:
 
+```ts
+/**
+ * Конструктор класса.
+ */
+interface Constructor<T extends object = object> {
+  new (...args: any[]): T;
+}
+```
+
 ### serviceContainer.get
 
 Метод `get` класса `ServiceContainer` создает экземпляр
@@ -202,10 +363,15 @@ const myService = container.get(MyService);
 
 Сигнатура:
 
-- `get(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает новый или существующий экземпляр сервиса;
+```ts
+/**
+ * Получить существующий или новый экземпляр.
+ *
+ * @param ctor
+ * @param args
+ */
+get<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
+```
 
 Пример:
 
@@ -245,10 +411,16 @@ console.log(myDate3); // Sun May 05 2030 03:00:00
 
 Сигнатура:
 
-- `getRegistered(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает новый или существующий экземпляр сервиса;
+```ts
+/**
+ * Получить существующий или новый экземпляр,
+ * только если конструктор зарегистрирован.
+ *
+ * @param ctor
+ * @param args
+ */
+getRegistered<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
+```
 
 Пример:
 
@@ -273,9 +445,14 @@ container.getRegistered(UnregisteredService);
 
 Сигнатура:
 
-- `has(ctor)`
-  - `ctor: Function`: класс сервиса;
-  - возвращает `true` если класс зарегистрирован;
+```ts
+/**
+ * Проверить существование конструктора в контейнере.
+ *
+ * @param ctor
+ */
+has<T extends object>(ctor: Constructor<T>): boolean;
+```
 
 Пример:
 
@@ -297,10 +474,15 @@ console.log(container.has(MyService)); // true
 
 Сигнатура:
 
-- `add(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает экземпляр сервис-контейнера;
+```ts
+/**
+ * Добавить конструктор в контейнер.
+ *
+ * @param ctor
+ * @param args
+ */
+add<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
+```
 
 Пример:
 
@@ -329,10 +511,15 @@ console.log('After get');
 
 Сигнатура:
 
-- `use(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает новый экземпляр сервиса;
+```ts
+/**
+ * Добавить конструктор и создать экземпляр.
+ *
+ * @param ctor
+ * @param args
+ */
+use<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
+```
 
 Пример:
 
@@ -359,10 +546,15 @@ const service = container.get(MyService); // возвращает готовый
 
 Сигнатура:
 
-- `set(ctor, service)`
-  - `ctor: Function`: класс сервиса;
-  - `service: object`: экземпляр сервиса;
-  - возвращает экземпляр сервис-контейнера;
+```ts
+/**
+ * Добавить конструктор и связанный экземпляр.
+ *
+ * @param ctor
+ * @param service
+ */
+set<T extends object>(ctor: Constructor<T>, service: T): this;
+```
 
 Пример:
 
@@ -395,10 +587,26 @@ console.log(api === mock); // true
 
 Сигнатура:
 
-- `find(predicate, [noParent])`
-  - `predicate: Function`: функция-предикат для проверки конструктора;
-  - `noParent?: boolean`: отключить поиск в родительских контейнерах;
-  - возвращает найденный экземпляр или `undefined`;
+```ts
+/**
+ * Найти сервис удовлетворяющий условию.
+ *
+ * @param predicate
+ * @param noParent
+ */
+find<T extends object>(
+  predicate: FindServicePredicate<T>,
+  noParent?: boolean,
+): T | undefined;
+
+/**
+ * Определение функции-предиката.
+ */
+type FindServicePredicate<T extends object> = (
+  ctor: Constructor<T>,
+  container: ServiceContainer,
+) => boolean;
+```
 
 Пример:
 
@@ -450,6 +658,20 @@ console.log(service2); // undefined
 проверяет наличие родителя, а `getParent` возвращает его или выбрасывает
 ошибку, если родителя нет.
 
+Сигнатура:
+
+```ts
+/**
+ * Получить родительский сервис-контейнер или выбросить ошибку.
+ */
+getParent(): ServiceContainer;
+
+/**
+ * Проверить наличие родительского сервис-контейнера.
+ */
+hasParent(): boolean;
+```
+
 Пример:
 
 ```js
@@ -497,7 +719,7 @@ console.log(hasService); // true
 - [`addService(ctor, ...args)`](#serviceaddservice) добавить конструктор в контейнер;
 - [`useService(ctor, ...args)`](#serviceuseservice) добавить конструктор и создать экземпляр;
 - [`setService(ctor, service)`](#servicesetservice) добавить конструктор и его экземпляр;
-- [`findService(predicate, [noParent])`](#servicefindservice) найти сервис удовлетворяющий условию;
+- [`findService(predicate, noParent = false)`](#servicefindservice) найти сервис удовлетворяющий условию;
 
 Сервисом может являться совершенно любой класс. Однако, если это
 наследник класса `Service`, то такой сервис позволяет инкапсулировать
@@ -552,10 +774,15 @@ const app = new App();
 
 Сигнатура:
 
-- `getService(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает новый или существующий экземпляр сервиса;
+```ts
+/**
+ * Получить существующий или новый экземпляр.
+ *
+ * @param ctor
+ * @param args
+ */
+getService<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
+```
 
 Пример:
 
@@ -577,10 +804,19 @@ console.log(foo3 === foo4);               // true
 
 Сигнатура:
 
-- `getRegisteredService(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает новый или существующий экземпляр сервиса;
+```ts
+/**
+ * Получить существующий или новый экземпляр,
+ * только если конструктор зарегистрирован.
+ *
+ * @param ctor
+ * @param args
+ */
+getRegisteredService<T extends object>(
+  ctor: Constructor<T>,
+  ...args: any[],
+): T;
+```
 
 Пример:
 
@@ -608,9 +844,14 @@ class MyService extends Service {
 
 Сигнатура:
 
-- `hasService(ctor)`
-  - `ctor: Function`: класс сервиса;
-  - возвращает `true` если класс зарегистрирован;
+```ts
+/**
+ * Проверка существования конструктора в контейнере.
+ *
+ * @param ctor
+ */
+hasService<T extends object>(ctor: Constructor<T>): boolean;
+```
 
 Пример:
 
@@ -635,10 +876,15 @@ class MyService extends Service {
 
 Сигнатура:
 
-- `addService(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает экземпляр сервис-контейнера;
+```ts
+/**
+ * Добавить конструктор в контейнер.
+ *
+ * @param ctor
+ * @param args
+ */
+addService<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
+```
 
 Пример:
 
@@ -662,10 +908,15 @@ class App extends Service {
 
 Сигнатура:
 
-- `useService(ctor, ...args)`
-  - `ctor: Function`: класс сервиса;
-  - `...args: *[]`: аргументы конструктора;
-  - возвращает новый экземпляр сервиса;
+```ts
+/**
+ * Добавить конструктор и создать экземпляр.
+ *
+ * @param ctor
+ * @param args
+ */
+useService<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
+```
 
 Пример:
 
@@ -692,10 +943,15 @@ class App extends Service {
 
 Сигнатура:
 
-- `setService(ctor, service)`
-  - `ctor: Function`: класс сервиса;
-  - `service: object`: экземпляр сервиса;
-  - возвращает экземпляр сервис-контейнера;
+```ts
+/**
+ * Добавить конструктор и связанный экземпляр.
+ *
+ * @param ctor
+ * @param service
+ */
+setService<T extends object>(ctor: Constructor<T>, service: T): this;
+```
 
 Пример:
 
@@ -724,10 +980,26 @@ class MyComponent extends Service {
 
 Сигнатура:
 
-- `findService(predicate, [noParent])`
-  - `predicate: Function`: функция-предикат для проверки конструктора;
-  - `noParent?: boolean`: отключить поиск в родительских контейнерах;
-  - возвращает найденный экземпляр или `undefined`;
+```ts
+/**
+ * Найти сервис удовлетворяющий условию.
+ *
+ * @param predicate
+ * @param noParent
+ */
+findService<T extends object>(
+  predicate: FindServicePredicate<T>,
+  noParent?: boolean,
+): T | undefined;
+
+/**
+ * Определение функции-предиката.
+ */
+type FindServicePredicate<T extends object> = (
+  ctor: Constructor<T>,
+  container: ServiceContainer,
+) => boolean;
+```
 
 Пример:
 

+ 5 - 3
dist/cjs/index.cjs

@@ -44,13 +44,15 @@ var _ServiceContainer = class _ServiceContainer {
   /**
    * Services map.
    *
-   * @type {Map<*, *>}
+   * @type {Map<any, any>}
+   * @private
    */
   _services = /* @__PURE__ */ new Map();
   /**
    * Parent container.
    *
    * @type {ServiceContainer}
+   * @private
    */
   _parent;
   /**
@@ -210,7 +212,7 @@ var _ServiceContainer = class _ServiceContainer {
   /**
    * Найти сервис удовлетворяющий условию.
    *
-   * @param {Function} predicate
+   * @param {function(Function, ServiceContainer): boolean} predicate
    * @param {boolean} noParent
    * @returns {*}
    */
@@ -338,7 +340,7 @@ var _Service = class _Service {
   /**
    * Найти сервис удовлетворяющий условию.
    *
-   * @param {Function} predicate
+   * @param {function(Function, ServiceContainer): boolean} predicate
    * @param {boolean} noParent
    * @returns {*}
    */

+ 2 - 5
eslint.config.js

@@ -2,7 +2,6 @@ import globals from 'globals';
 import eslintJs from '@eslint/js';
 import eslintJsdocPlugin from 'eslint-plugin-jsdoc';
 import eslintMochaPlugin from 'eslint-plugin-mocha';
-import eslintImportPlugin from 'eslint-plugin-import';
 import eslintPrettierConfig from 'eslint-config-prettier';
 import eslintChaiExpectPlugin from 'eslint-plugin-chai-expect';
 
@@ -11,22 +10,20 @@ export default [{
     globals: {
       ...globals.es2021,
       ...globals.mocha,
+      ...globals.node,
     },
   },
   plugins: {
     'jsdoc': eslintJsdocPlugin,
     'mocha': eslintMochaPlugin,
-    'import': eslintImportPlugin,
     'chai-expect': eslintChaiExpectPlugin,
   },
   rules: {
     ...eslintJs.configs.recommended.rules,
     ...eslintPrettierConfig.rules,
-    ...eslintImportPlugin.flatConfigs.recommended.rules,
+    ...eslintJsdocPlugin.configs['flat/recommended-error'].rules,
     ...eslintMochaPlugin.configs.recommended.rules,
     ...eslintChaiExpectPlugin.configs['recommended-flat'].rules,
-    ...eslintJsdocPlugin.configs['flat/recommended-error'].rules,
-    'no-duplicate-imports': 'error',
     'jsdoc/reject-any-type': 0,
     'jsdoc/reject-function-type': 0,
     'jsdoc/require-param-description': 0,

+ 13 - 11
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@e22m4u/js-service",
-  "version": "0.5.0",
+  "version": "0.4.6",
   "description": "Реализация принципа инверсии управления для JavaScript",
   "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
   "license": "MIT",
@@ -11,15 +11,17 @@
     "Locator",
     "Container"
   ],
-  "homepage": "https://gitrepos.ru/e22m4u/js-service",
+  "homepage": "https://github.com/e22m4u/js-service",
   "repository": {
     "type": "git",
-    "url": "git+https://gitrepos.ru/e22m4u/js-service.git"
+    "url": "git+https://github.com/e22m4u/js-service.git"
   },
   "type": "module",
+  "types": "./src/index.d.ts",
   "module": "./src/index.js",
   "main": "./dist/cjs/index.cjs",
   "exports": {
+    "types": "./src/index.d.ts",
     "import": "./src/index.js",
     "require": "./dist/cjs/index.cjs"
   },
@@ -27,17 +29,17 @@
     "node": ">=12"
   },
   "scripts": {
-    "lint": "eslint ./src",
-    "lint:fix": "eslint ./src --fix",
+    "lint": "tsc && eslint ./src",
+    "lint:fix": "tsc && eslint ./src --fix",
     "format": "prettier --write \"./src/**/*.js\"",
     "test": "npm run lint && c8 --reporter=text-summary mocha",
     "test:coverage": "npm run lint && c8 --reporter=text mocha",
-    "build:cjs": "rimraf ./dist/cjs && node build-cjs.js",
+    "build:cjs": "rimraf ./dist/cjs && node --no-warnings=ExperimentalWarning build-cjs.js",
     "prepare": "husky"
   },
   "dependencies": {
-    "@e22m4u/js-debug": "~0.4.0",
-    "@e22m4u/js-format": "~0.3.0"
+    "@e22m4u/js-debug": "~0.3.3",
+    "@e22m4u/js-format": "~0.2.1"
   },
   "devDependencies": {
     "@commitlint/cli": "~20.1.0",
@@ -50,13 +52,13 @@
     "eslint": "~9.39.1",
     "eslint-config-prettier": "~10.1.8",
     "eslint-plugin-chai-expect": "~3.1.0",
-    "eslint-plugin-import": "^2.32.0",
     "eslint-plugin-jsdoc": "~61.4.1",
     "eslint-plugin-mocha": "~11.2.0",
     "globals": "~16.5.0",
     "husky": "~9.1.7",
     "mocha": "~11.7.5",
-    "prettier": "~3.7.3",
-    "rimraf": "~6.1.2"
+    "prettier": "~3.6.2",
+    "rimraf": "~6.1.2",
+    "typescript": "~5.9.3"
   }
 }

+ 107 - 0
src/debuggable-service.d.ts

@@ -0,0 +1,107 @@
+import {Service} from './service.js';
+import {Constructor} from './types.js';
+import {Debuggable} from '@e22m4u/js-debug';
+import {DebuggableOptions} from '@e22m4u/js-debug';
+import {FindServicePredicate, ServiceContainer} from './service-container.js';
+
+/**
+ * Debuggable service.
+ */
+export class DebuggableService extends Debuggable implements Service {
+  /**
+   * Kind.
+   */
+  static readonly kinds: string[];
+
+  /**
+   * Container.
+   */
+  container: ServiceContainer;
+
+  /**
+   * Constructor.
+   *
+   * @param container
+   * @param options
+   */
+  constructor(
+    container?: ServiceContainer,
+    options?: DebuggableOptions,
+  );
+
+  /**
+   * Получить существующий или новый экземпляр.
+   *
+   * @param ctor
+   * @param args
+   */
+  getService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): T;
+  
+  /**
+   * Получить существующий или новый экземпляр,
+   * только если конструктор зарегистрирован.
+   *
+   * @param ctor
+   * @param args
+   */
+  getRegisteredService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): T;
+
+  /**
+   * Проверка существования конструктора в контейнере.
+   *
+   * @param ctor
+   */
+  hasService<T extends object>(
+    ctor: Constructor<T>,
+  ): boolean;
+
+  /**
+   * Добавить конструктор в контейнер.
+   *
+   * @param ctor
+   * @param args
+   */
+  addService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): this;
+
+  /**
+   * Добавить конструктор и создать экземпляр.
+   *
+   * @param ctor
+   * @param args
+   */
+  useService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): this;
+
+  /**
+   * Добавить конструктор и связанный экземпляр.
+   *
+   * @param ctor
+   * @param service
+   */
+  setService<T extends object>(
+    ctor: Constructor<T>,
+    service: T,
+  ): this;
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param predicate
+   * @param noParent
+   */
+  findService<T extends object>(
+    predicate: FindServicePredicate<T>,
+    noParent?: boolean,
+  ): T | undefined;
+}

+ 1 - 0
src/errors/index.d.ts

@@ -0,0 +1 @@
+export * from './invalid-argument-error.js';

+ 6 - 0
src/errors/invalid-argument-error.d.ts

@@ -0,0 +1,6 @@
+import {Errorf} from '@e22m4u/js-format';
+
+/**
+ * Invalid argument error.
+ */
+export declare class InvalidArgumentError extends Errorf {}

+ 5 - 0
src/index.d.ts

@@ -0,0 +1,5 @@
+export * from './types.js';
+export * from './service.js';
+export * from './service-container.js';
+export * from './debuggable-service.js';
+export * from './utils/is-service-container.js';

+ 1 - 0
src/index.js

@@ -1,3 +1,4 @@
+export * from './types.js';
 export * from './service.js';
 export * from './service-container.js';
 export * from './debuggable-service.js';

+ 105 - 0
src/service-container.d.ts

@@ -0,0 +1,105 @@
+import {Constructor} from './types.js';
+
+/**
+ * Find service predicate.
+ */
+export type FindServicePredicate<T extends object> = (
+  ctor: Constructor<T>,
+  container: ServiceContainer,
+) => boolean;
+
+/**
+ * Service container.
+ */
+export declare class ServiceContainer {
+  /**
+   * Constructor.
+   *
+   * @param parent
+   */
+  constructor(parent?: ServiceContainer);
+
+  /**
+   * Получить родительский сервис-контейнер или выбросить ошибку.
+   */
+  getParent(): ServiceContainer;
+
+  /**
+   * Проверить наличие родительского сервис-контейнера.
+   */
+  hasParent(): boolean;
+
+  /**
+   * Получить существующий или новый экземпляр.
+   *
+   * @param ctor
+   * @param args
+   */
+  get<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): T;
+
+  /**
+   * Получить существующий или новый экземпляр,
+   * только если конструктор зарегистрирован.
+   *
+   * @param ctor
+   * @param args
+   */
+  getRegistered<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): T;
+
+  /**
+   * Проверить существование конструктора в контейнере.
+   *
+   * @param ctor
+   */
+  has<T extends object>(ctor: Constructor<T>): boolean;
+
+  /**
+   * Добавить конструктор в контейнер.
+   *
+   * @param ctor
+   * @param args
+   */
+  add<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): this;
+
+  /**
+   * Добавить конструктор и создать экземпляр.
+   *
+   * @param ctor
+   * @param args
+   */
+  use<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): this;
+
+  /**
+   * Добавить конструктор и связанный экземпляр.
+   *
+   * @param ctor
+   * @param service
+   */
+  set<T extends object>(
+    ctor: Constructor<T>,
+    service: T,
+  ): this;
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param predicate
+   * @param noParent
+   */
+  find<T extends object>(
+    predicate: FindServicePredicate<T>,
+    noParent?: boolean,
+  ): T | undefined;
+}

+ 4 - 2
src/service-container.js

@@ -22,7 +22,8 @@ export class ServiceContainer {
   /**
    * Services map.
    *
-   * @type {Map<*, *>}
+   * @type {Map<any, any>}
+   * @private
    */
   _services = new Map();
 
@@ -30,6 +31,7 @@ export class ServiceContainer {
    * Parent container.
    *
    * @type {ServiceContainer}
+   * @private
    */
   _parent;
 
@@ -240,7 +242,7 @@ export class ServiceContainer {
   /**
    * Найти сервис удовлетворяющий условию.
    *
-   * @param {Function} predicate
+   * @param {function(Function, ServiceContainer): boolean} predicate
    * @param {boolean} noParent
    * @returns {*}
    */

+ 2 - 5
src/service-container.spec.js

@@ -2,11 +2,8 @@ import {expect} from 'chai';
 import {Service} from './service.js';
 import {format} from '@e22m4u/js-format';
 import {createSpy} from '@e22m4u/js-spy';
-
-import {
-  ServiceContainer,
-  SERVICE_CONTAINER_CLASS_NAME,
-} from './service-container.js';
+import {ServiceContainer} from './service-container.js';
+import {SERVICE_CONTAINER_CLASS_NAME} from './service-container.js';
 
 describe('ServiceContainer', function () {
   it('should expose static property "kinds"', function () {

+ 105 - 0
src/service.d.ts

@@ -0,0 +1,105 @@
+import {Constructor} from './types.js';
+import {FindServicePredicate, ServiceContainer} from './service-container.js';
+
+/**
+ * Service class name.
+ */
+export const SERVICE_CLASS_NAME: 'Service';
+
+/**
+ * Service.
+ */
+export declare class Service {
+  /**
+   * Kind.
+   */
+  static readonly kinds: string[];
+
+  /**
+   * Container.
+   */
+  container: ServiceContainer;
+
+  /**
+   * Constructor.
+   *
+   * @param container
+   */
+  constructor(container?: ServiceContainer);
+
+  /**
+   * Получить существующий или новый экземпляр.
+   *
+   * @param ctor
+   * @param args
+   */
+  getService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): T;
+
+  /**
+   * Получить существующий или новый экземпляр,
+   * только если конструктор зарегистрирован.
+   *
+   * @param ctor
+   * @param args
+   */
+  getRegisteredService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): T;
+
+  /**
+   * Проверка существования конструктора в контейнере.
+   *
+   * @param ctor
+   */
+  hasService<T extends object>(
+    ctor: Constructor<T>,
+  ): boolean;
+
+  /**
+   * Добавить конструктор в контейнер.
+   *
+   * @param ctor
+   * @param args
+   */
+  addService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): this;
+
+  /**
+   * Добавить конструктор и создать экземпляр.
+   *
+   * @param ctor
+   * @param args
+   */
+  useService<T extends object>(
+    ctor: Constructor<T>,
+    ...args: any[],
+  ): this;
+
+  /**
+   * Добавить конструктор и связанный экземпляр.
+   *
+   * @param ctor
+   * @param service
+   */
+  setService<T extends object>(
+    ctor: Constructor<T>,
+    service: T,
+  ): this;
+
+  /**
+   * Найти сервис удовлетворяющий условию.
+   *
+   * @param predicate
+   * @param noParent
+   */
+  findService<T extends object>(
+    predicate: FindServicePredicate<T>,
+    noParent?: boolean,
+  ): T | undefined;
+}

+ 1 - 1
src/service.js

@@ -109,7 +109,7 @@ export class Service {
   /**
    * Найти сервис удовлетворяющий условию.
    *
-   * @param {Function} predicate
+   * @param {function(Function, ServiceContainer): boolean} predicate
    * @param {boolean} noParent
    * @returns {*}
    */

+ 4 - 6
src/service.spec.js

@@ -1,10 +1,8 @@
 import {expect} from 'chai';
-import {Service, SERVICE_CLASS_NAME} from './service.js';
-
-import {
-  ServiceContainer,
-  SERVICE_CONTAINER_CLASS_NAME,
-} from './service-container.js';
+import {Service} from './service.js';
+import {SERVICE_CLASS_NAME} from './service.js';
+import {ServiceContainer} from './service-container.js';
+import {SERVICE_CONTAINER_CLASS_NAME} from './service-container.js';
 
 describe('Service', function () {
   it('should expose static property "kinds"', function () {

+ 7 - 0
src/types.d.ts

@@ -0,0 +1,7 @@
+/**
+ * A callable type with "new" operator allows
+ * class and constructor.
+ */
+export interface Constructor<T extends object = object> {
+  new (...args: any[]): T;
+}

+ 1 - 0
src/types.js

@@ -0,0 +1 @@
+export {};

+ 1 - 0
src/utils/index.d.ts

@@ -0,0 +1 @@
+export * from './is-service-container.js';

+ 10 - 0
src/utils/is-service-container.d.ts

@@ -0,0 +1,10 @@
+import {ServiceContainer} from '../service-container.js';
+
+/**
+ * Определяет, является ли аргумент сервис-контейнером.
+ *
+ * @param container
+ */
+export declare function isServiceContainer(
+  container: unknown,
+): container is ServiceContainer;

+ 4 - 4
src/utils/is-service-container.js

@@ -10,9 +10,9 @@ import {SERVICE_CONTAINER_CLASS_NAME} from '../service-container.js';
 export function isServiceContainer(container) {
   return Boolean(
     container &&
-    typeof container === 'object' &&
-    typeof container.constructor === 'function' &&
-    Array.isArray(container.constructor.kinds) &&
-    container.constructor.kinds.includes(SERVICE_CONTAINER_CLASS_NAME),
+      typeof container === 'object' &&
+      typeof container.constructor === 'function' &&
+      Array.isArray(container.constructor.kinds) &&
+      container.constructor.kinds.includes(SERVICE_CONTAINER_CLASS_NAME),
   );
 }

+ 2 - 5
src/utils/is-service-container.spec.js

@@ -1,10 +1,7 @@
 import {expect} from 'chai';
+import {ServiceContainer} from '../service-container.js';
 import {isServiceContainer} from './is-service-container.js';
-
-import {
-  ServiceContainer,
-  SERVICE_CONTAINER_CLASS_NAME,
-} from '../service-container.js';
+import {SERVICE_CONTAINER_CLASS_NAME} from '../service-container.js';
 
 describe('isServiceContainer', function () {
   it('should return true for ServiceContainer instance', function () {

+ 2 - 0
jsconfig.json → tsconfig.json

@@ -1,5 +1,7 @@
 {
   "compilerOptions": {
+    "rootDir": "src",
+    "noEmit": true,
     "target": "es2022",
     "module": "NodeNext",
     "moduleResolution": "NodeNext"