События
Модули общаются через версионированную шину событий, а не вызывают друг друга напрямую. Ваш модуль объявляет в манифесте, что он публикует и на что подписывается; платформа проверяет эти объявления до того, как вы выпустите релиз.
Объявление в манифесте
"events": {
"publishes": [
{ "name": "cafm.work-order.created", "version": "1.0.0" }
],
"subscribes": [
{ "name": "building.alarm.triggered", "version": "1.0.0" }
]
}Откуда берутся события
У событий два источника, и от того, кто издаёт событие, которое вы читаете, зависит выбор инструмента ниже:
- События платформы (
building.*) публикует сама платформа. Они версионируются вместе с SDK в его снимке каталога (tv-events.snapshot.json) — полный список со схемами полезной нагрузки: События платформы. - События модулей (
cafm.*,service-desk.*, …) публикуют отдельные модули. Центрального каталога для них нет — так задумано: события, публикуемые модулем, объявляются в манифесте этого модуля (events.publishes) и больше нигде, поэтому не существует реестра, куда нужно добиваться добавления своего события. Кто что публикует и читает — в каталоге модулей.
Проверка ваших подписок — check-pact
check-pact проверяет, что каждый контракт в events.subscribes действительно совместим со схемой издателя:
# Если вы подписаны только на первичные события платформы:
npx @tv/extension-sdk check-pact module-manifest.json
# Если вы подписаны на событие, публикуемое другим МОДУЛЕМ, — подмешайте
# его манифест, потому что событий модулей нет в первичном каталоге:
npx @tv/extension-sdk check-pact module-manifest.json \
--producer=../tv-module-cafm/module-manifest.jsonПередавайте по одному --producer на каждый издающий модуль. Без этого подписка на событие модуля падает как «неизвестное событие» — это корректная работа инструмента, а не ошибка.
Защита ваших собственных событий — check-events
Если вы публикуете события, зафиксируйте снимок их схем и закоммитьте его. check-events сравнивает текущие схемы со снимком и валит CI на любом ломающем изменении — до того, как оно дойдёт до ваших потребителей:
npx @tv/extension-sdk snapshot-events ./tv-events.snapshot.json # перегенерировать
npx @tv/extension-sdk check-events ./tv-events.snapshot.json # проверить в CIОбе команды принимают путь к снимку аргументом.
Публикация и подписка во время выполнения
Доступ к шине — через поле eventBus контекста платформы:
import { usePlatformContext } from '@tv/extension-sdk/react';
import { useEffect } from 'react';
function useWorkOrderEvents() {
const { eventBus } = usePlatformContext();
// публикация — (имя события, полезная нагрузка)
const announce = (wo: WorkOrder) =>
eventBus.publish('cafm.work-order.created', {
id: wo.id,
buildingId: wo.buildingId,
});
// подписка — возвращает функцию отписки; вызовите её при размонтировании
useEffect(() => {
return eventBus.subscribe<AlarmEvent>('building.alarm.triggered', (event) => {
// реакция на тревогу
});
}, [eventBus]);
return { announce };
}eventBus, а не events
Поле контекста называется eventBus. publish принимает полезную нагрузку напрямую вторым аргументом — version события живёт в объявлении в манифесте, а не в каждом вызове.
Транспорт
EventBusClient — стабильный интерфейс поверх меняющегося транспорта: сейчас он проксирует на WebSocket-шлюз платформы и переезжает на NATS JetStream. Ваш код при этом не меняется — в этом и смысл интерфейса.
Нужен ответ? Это не событие
publish() работает по принципу «отправил и забыл»: он возвращает Promise<void>, который резолвится, когда шина приняла событие, — а не когда (и не «если») его обработал какой-то потребитель. Режима запрос-ответ у шины нет.
Когда коду нужен результат, парой «запрос-ответ» служит HTTP-вызов через ctx.api к модулю-владельцу данных; событие — это то, как о случившемся узнают все остальные:
const { api, eventBus } = usePlatformContext();
// Вызов API и ЕСТЬ запрос-ответ — созданная сущность приходит в ответе.
const wo = await api.post<WorkOrder>(
`/api/v1/buildings/${building.id}/work-orders`,
dto,
);
// Событие — объявление для других модулей; на него никто не «отвечает».
await eventBus.publish('cafm.work-order.created', {
id: wo.id,
buildingId: wo.buildingId,
});Правило: API — для вопросов, события — для объявлений. Если вы ловите себя на том, что публикуете событие и ждёте «ответное», замените эту пару одним вызовом API.
Версионирование
Имена событий имеют пространство имён (<module>.<entity>.<action>) и несут semver-version. Несовместимое изменение формы полезной нагрузки означает новую мажорную версию этого события — потребители фиксируют ту версию, которую понимают, поэтому вы можете развивать события, не ломая их.
→ Далее: Тестирование