Skip to content

События ​

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

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

json
"events": {
  "publishes": [
    { "name": "cafm.work-order.created", "version": "1.0.0" }
  ],
  "subscribes": [
    { "name": "building.alarm.triggered", "version": "1.0.0" }
  ]
}

Скажите, что читаете и что обещаете, — payloadContract ​

Любая из двух сторон объявления события может нести payloadContract. Именно он позволяет check-pact сказать что-то содержательное, и именно его чаще всего забывают указать.

json
"events": {
  "publishes": [
    {
      "name": "cafm.work-order.created",
      "version": "1.0.0",
      "payloadContract": {
        "type": "object",
        "required": ["workOrderId", "buildingId"],
        "properties": {
          "workOrderId": { "type": "string" },
          "buildingId":  { "type": "string" },
          "priority":    { "type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "URGENT"] }
        }
      }
    }
  ],
  "subscribes": [
    {
      "name": "building.alarm.triggered",
      "version": "1.0.0",
      "payloadContract": {
        "type": "object",
        "required": ["buildingId", "severity"],
        "properties": {
          "buildingId": { "type": "string", "reason": "Scopes the work order we open." },
          "severity":   { "type": "string", "reason": "Maps onto work-order priority." }
        }
      }
    }
  ]
}

Стороны означают намеренно разное:

  • В subscribes это то подмножество полезной нагрузки издателя, которое вы действительно читаете. check-pact завалит проверку, если издатель не даёт объявленное вами поле, — ради этого всё и затевалось. reason — свободный текст о том, зачем вам поле; пишите его для сопровождающего со стороны издателя, который увидит эту строку, когда соберётся менять нагрузку.
  • В publishes это то, что вы гарантируете отправлять. По нему проверяют себя потребители.

Запись в publishes без payloadContract всё равно регистрирует событие: вы его публикуете, значит подписчик никогда не должен получить EVENT_NOT_PUBLISHED. Вместо этого запись помечается как непрозрачная, и check-pact сообщает о ней информационным PRODUCER_PAYLOAD_OPAQUE — пробел в покрытии виден, но ничей CI при этом не краснеет.

Откуда берутся события ​

У событий два источника, и от того, кто издаёт событие, которое вы читаете, зависит выбор инструмента ниже:

  • События платформы (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

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

События платформы в интерфейсе модуля ​

Фронтенд модуля получает события платформы через usePlatformEvent из @tv/extension-sdk 1.18.0 или новее. Он имеет уровень @experimental, как и subscribePlatformEvent и createMockPlatformEvents ниже: полезны, но могут измениться в любом релизе (Уровни стабильности).

tsx
import { usePlatformEvent } from '@tv/extension-sdk/react';
import { useState } from 'react';

export function AlarmBanner() {
  const [alarm, setAlarm] = useState<string | null>(null);

  // Тревоги по зданию, выбранному в шапке оболочки, по мере поступления.
  usePlatformEvent('building.alarm.triggered', (a) => {
    setAlarm(`${a.severity}: ${a.title}`);
  });

  return alarm ? <p role="status">{alarm}</p> : null;
}

Что приходит и почему:

  • Семь событий — те, что tv-api отправляет в браузер: building.point.updated, building.alarm.triggered, building.fault.created и четыре события wifi-sensing.*. Их полезная нагрузка типизирована; для событий building.* см. События платформы. Внутри оболочки и с тестовой заглушкой (ниже) подписка на что-то другое выбрасывает TypeError. События, которые публикуют другие модули, в браузер не попадают вовсе.
  • Только по зданию, выбранному в шапке, и только то, что вошедшему пользователю можно читать. Тревогам и неисправностям (building.alarm.triggered, building.fault.created) нужно building.faults:read, показаниям (building.point.updated) — telemetry.points:subscribe, ровно как в REST API. Событиям wifi-sensing.* разрешение не нужно: достаточно войти в организацию, которой принадлежит здание, — как и для чтения данных WiFi-сенсинга через REST.
  • Собственная копия каждой полезной нагрузки. Другой модуль не может её изменить.
  • Публиковать нечего. Фронтенд модуля не публикует события: меняйте состояние через API, а объявлять о случившемся будет бэкенд, которому принадлежат данные (ниже).

Вне оболочки (автономная страница под pnpm dev, модульный тест без заглушки) и в оболочке, где этого канала ещё нет, ничего не приходит и ничего не выбрасывается: подписка ничего не делает, какой бы тип вы ни передали, поэтому тот же код там работает и ничего не делает. isPlatformEventsAvailable() из @tv/extension-sdk/context (тоже @experimental) показывает, какой это случай. В тестах поставьте заглушку и отправляйте события сами:

ts
import { createMockPlatformEvents } from '@tv/extension-sdk/testing';

const events = createMockPlatformEvents();
const uninstall = events.install();   // нужна тестовая среда с DOM
// отрендерите компонент, затем:
events.emit('building.alarm.triggered', {
  buildingId, faultId, severity: 'high', title: 'Приточный воздух слишком тёплый',
});
uninstall();

Вне React то же делает subscribePlatformEvent(type, handler) из @tv/extension-sdk/context; он возвращает функцию отписки (пустую, если оболочка событий не доставляет).

Не usePlatformContext().eventBus

usePlatformContext() внутри оболочки выбрасывает ошибку: React-контекст не пересекает границу федерации (Федерация и оболочка). Используйте usePlatformEvent.

Публикация и получение в бэкенде ​

События модулей (cafm.work-order.created и подобные) ходят между бэкендами модулей через NATS JetStream, а не через браузер. Субъект — tv.building.<buildingId>.<событие>, сообщение — конверт вокруг вашей полезной нагрузки; оба строит SDK:

ts
import { buildSubject, createEnvelope } from '@tv/extension-sdk/events';

const subject = buildSubject(wo.buildingId, 'cafm.work-order.created');
const envelope = createEnvelope({
  type: 'cafm.work-order.created',
  buildingId: wo.buildingId,
  producer: 'cafm',
  payload: { workOrderId: wo.id, buildingId: wo.buildingId, priority: wo.priority },
});
// опубликуйте `envelope` в `subject` своим клиентом NATS

Бэкенд подписывается так же, на tv.building.*.<событие> по всем зданиям. События платформы (building.*) приходят туда же.

Пока не в песочнице

В песочницах нет сервера NATS, а ваш бэкенд работает на вашей машине, поэтому публиковать и получать события модулей в песочнице сейчас нельзя. Разрабатывайте обработчик на полезных нагрузках, которые строите в тесте, и объявите события в манифесте: check-pact проверит вашу сторону каждого контракта.

Нужен ответ? Это не событие ​

Событие — это объявление. На него никто не отвечает, и публикация ничего не говорит о том, обработал ли его кто-нибудь и когда.

Когда коду нужен результат, вызовите API модуля, которому принадлежат данные: createPlatformApiClient() для ресурсов tv-api (здания, помещения, неисправности — см. клиент api), ваш собственный бэкенд (Добавьте бэкенд) — для собственных ресурсов модуля. Событие — это то, как о случившемся узнают все остальные:

ts
import { createPlatformApiClient } from '@tv/extension-sdk/context';

const api = createPlatformApiClient();

// Вызов API и ЕСТЬ запрос-ответ — созданная сущность приходит в ответе.
// `createPlatformApiClient()` доходит только до tv-api; заявки — собственный
// ресурс модуля CAFM, поэтому вызов идёт в его собственный бэкенд (см.
// «Добавьте бэкенд»), а не через этот клиент.
const wo: WorkOrder = await createWorkOrder(dto);

// Бэкенд CAFM, которому принадлежат заявки, затем публикует
// cafm.work-order.created для других модулей.

Правило: API — для вопросов, события — для объявлений. Если вы ловите себя на том, что публикуете событие и ждёте «ответное», замените эту пару одним вызовом API.

Версионирование ​

Имена событий имеют пространство имён (<module>.<entity>.<action>) и несут semver-version. Несовместимое изменение формы полезной нагрузки означает новую мажорную версию этого события — потребители фиксируют ту версию, которую понимают, поэтому вы можете развивать события, не ломая их.

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

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