Cordis и плагинная архитектура в TypeScript без головной боли

16 авг 2026
4,615
240
21
1 месяц

Если вы когда-нибудь писали расширяемое приложение на Node.js или TypeScript, вы наверняка наступали на одни и те же грабли. Пользователь или система подключает плагин. Плагин вешает пяток слушателей событий, запускает пару таймеров setInterval, регистрирует свои роуты и внедряет сервис. А потом плагин отключают или обновляют на лету.

Что происходит дальше? Правильно, утечки памяти. Слушатели висят, таймеры тикают в фоне, ссылки на контекст не дают сборщику мусора очистить память. В Node.js управление жизненным циклом зависимостей часто превращается в ручную рутину.

Некоторое время назад разработчики чат-бот фреймворка Koishi столкнулись ровно с этой задачей. Им требовалось создать ядро, в котором сотни сторонних плагинов могут запускаться, изолироваться, заменять друг друга и выгружаться без перезапуска процесса. Так появился фреймворк Cordis.

Что вообще такое Cordis

Создатели называют свой проект «meta-framework of spatiotemporal composability». Звучит заумно и претенциозно, но суть на деле приземленная.

Cordis объединяет контейнер внедрения зависимостей (IoC), шину событий и иерархическое дерево контекстов. Каждый плагин или сервис живет внутри своего контекста. Если этот контекст уничтожается, Cordis автоматически подчищает за ним абсолютно все ресурсы: снимает обработчики событий, глушит таймеры и удаляет созданные сервисы.

Реклама
import { Context } from 'cordis'

// Создаем корневой контекст приложения
const ctx = new Context()

// Регистрируем плагин
ctx.plugin((ctx) => {
  ctx.on('ready', () => {
    console.log('Плагин готов к работе')
  })

  // Этот интервал сам остановится при выгрузке плагина
  ctx.setInterval(() => {
    console.log('Тик таймера')
  }, 1000)
})

await ctx.start()

Здесь нет магии, но есть четкая дисциплина: если плагин использует методы ctx.on, ctx.setInterval или ctx.effect, фреймворк берет очистку сайд-эффектов на себя.

Как устроена модель контекстов

Центральное понятие в библиотеке — Context. Это не просто плоский объект с настройками, а ветвящееся дерево.

Когда вы вызываете ctx.plugin(), фреймворк порождает дочерний контекст (fork). Дочерний контекст наследует сервисы родителя, но хранит собственные ссылки на зарегистрированные ресурсы.

Root Context
├── Database Service (глобальный)
├── Plugin A (Fork Context)
   └── Event Listener: 'user-login'
└── Plugin B (Fork Context)
    └── HTTP Route: '/api/status'

Если вы отключите Plugin A, его дочерний контекст схлопнется. Обработчик user-login снимется из общей шины событий, при этом Database Service и Plugin B продолжат спокойно работать.

Сервисы и типизация в TypeScript

Сервисы в Cordis объявляются через наследование базового класса Service. Это делает их доступными прямо через свойства контекста с сохранением строгой типизации:

import { Context, Service } from 'cordis'

// Расширяем интерфейс контекста для автокомплита в IDE
declare module 'cordis' {
  interface Context {
    database: DatabaseService
  }
}

class DatabaseService extends Service {
  constructor(ctx: Context) {
    // Регистрируем сервис под именем 'database'
    super(ctx, 'database', true)
  }

  getUser(id: string) {
    return { id, name: 'Alex' }
  }
}

const ctx = new Context()
ctx.plugin(DatabaseService)

// В другом плагине можно объявить зависимость от сервиса:
ctx.plugin((ctx) => {
  // Код выполнится только тогда, когда сервис database загружен
  ctx.inject(['database'], (ctx) => {
    const user = ctx.database.getUser('123')
    console.log(user)
  })
})

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

Тонкая настройка областей видимости

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

Cordis вводит концепцию фильтров через вызов ctx.isolate() и свойства контекста. Вы можете ограничить видимость сервиса отдельной веткой дерева или задать предикат, отсекающий ненужные события:

// Создаем изолированную ветку для конкретной сессии
const sessionCtx = ctx.isolate('session')

sessionCtx.plugin((ctx) => {
  // Этот плагин не будет конфликтовать с сервисами других сессий
})

Для каких задач это подходит

Библиотека создавалась под специфический класс приложений. Не стоит тащить ее в обычный CRUD API на Fastify или Express, там она создаст лишний слой абстракции.

Зато Cordis отлично вписывается в следующие сценарии:

  1. Модульные CLI-утилиты и генераторы. Когда пользователи могут доставлять npm-пакеты, расширяющие команды или пайплайны сборки.
  2. Десктопные приложения на Electron/Tauri. Для организации системы аддонов и тем оформления, которые можно включать и отключать на лету без перезагрузки окна.
  3. Боты и интеграционные хабы. Если сервис общается с десятком разных платформ (Telegram, Discord, Slack), и каждый адаптер должен жить изолированной жизнью.
  4. Инструменты автоматизации. Где процессы конфигурируются пользователями динамически через веб-интерфейс или YAML-файлы.

Подводные камни и недостатки

Идеальных инструментов нет, и у Cordis хватает специфических нюансов:

  • Крутая кривая обучения. Документация написана сухим языком с обилием специфических терминов. Чтобы продраться сквозь концепции слияния скоупов и сайд-эффектов, придется внимательно почитать исходники.
  • Непривычный API. Привязка сервисов к объекту контекста через модуль мерджинга типов TypeScript может сначала сбивать с толку тех, кто привык к классическому NestJS или InversifyJS с их декораторами.
  • Привязка к ментальной модели. Если архитектура вашего проекта не предполагает частой динамической выгрузки кода, выгоды от встроенного менеджера эффектов нивелируются усложнением кода.

Стоит ли пробовать

Cordis — интересный инженерный проект с фокусом на управление жизненным циклом компонентов. Он элегантно закрывает вопрос утечек ресурсов при создании расширяемых систем.

Если вы проектируете систему с развитой экосистемой подключаемых плагинов и хотите получить надежный механизм отслеживания сайд-эффектов из коробки, форкните репозиторий cordiverse/cordis и изучите примеры в тестах. Это отличный образец того, как можно выстроить микроядерную архитектуру на чистом TypeScript.

🍪 Мы используем файлы cookie и сервис аналитики Яндекс.Метрика, чтобы сайт работал лучше. Продолжая пользоваться devtrends.ru, вы соглашаетесь с обработкой данных согласно Политике конфиденциальности.