Skip to content

События

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

Объявление в манифесте

json
"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 действительно совместим со схемой издателя:

bash
# Если вы подписаны только на первичные события платформы:
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 на любом ломающем изменении — до того, как оно дойдёт до ваших потребителей:

bash
npx @tv/extension-sdk snapshot-events ./tv-events.snapshot.json   # перегенерировать
npx @tv/extension-sdk check-events ./tv-events.snapshot.json      # проверить в CI

Обе команды принимают путь к снимку аргументом.

Публикация и подписка во время выполнения

Доступ к шине — через поле eventBus контекста платформы:

ts
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 к модулю-владельцу данных; событие — это то, как о случившемся узнают все остальные:

ts
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. Несовместимое изменение формы полезной нагрузки означает новую мажорную версию этого события — потребители фиксируют ту версию, которую понимают, поэтому вы можете развивать события, не ломая их.

→ Далее: Тестирование

Создано на платформе Tango Vision. Вопросы? developers@tango.vision