Анатомия триггера
createTrigger — единственный конструктор в публичном API. Каждый триггер выглядит так:
Восемь полей, три из них опциональные. Разберём каждое.
Обобщённый параметр схемы
Заголовок раздела «Обобщённый параметр схемы»<Schema> — единственный источник правды о наборе портов триггера. У него три опциональные карты:
Триггеры без событий запрещены на этапе компиляции — без события триггер никогда не запустится. А вот без условий или без действий — нормально и часто полезно: у многих аналитических триггеров одно событие и одно действие.
Пустой payload пишется как void:
id (обязательно)
Заголовок раздела «id (обязательно)»Уникальная строка, идентифицирующая триггер в рантайме. Её используют три вещи:
- Инспектор индексирует прогоны по
id. - DevTools подписывают действия как
triggery/<id>/fire. graph()(CLI / API рантайма) делает по одной ноде на каждыйid.
Id должен быть строковым литералом. Правило no-dynamic-id из @triggery/eslint-plugin это контролирует — динамические id ломают devtools, индексацию инспектора и статический экстрактор графа.
Id — это существительные в kebab-case, описывающие сценарий, а не событие. message-received читается лучше, чем on-new-message-show-toast или new-message. Имя, совпадающее с реальным продуктовым понятием, ещё и делает инспектор читаемым.
events (обязательно)
Заголовок раздела «events (обязательно)»Readonly-массив имён событий из схемы. Рантайм индексирует триггеры по имени события — рассматриваются только те триггеры, у которых в events указано сработавшее событие.
Если у триггера одно событие — укажи его. Если он реагирует на несколько — перечисли все: обработчик увидит event.name и event.payload, типизированные как объединение всех перечисленных событий.
Этот список — единственное, против чего eslint-правило exhaustive-conditions делает кросс-проверку. Если в файле вызывается useEvent(messageTrigger, 'foo'), но 'foo' нет в events, получишь lint-ошибку.
required (опционально)
Заголовок раздела «required (опционально)»Список имён условий, которые должны присутствовать, чтобы обработчик запустился.
Поведение:
- Перед запуском обработчика рантайм проверяет, что у каждого required-условия есть хотя бы один зарегистрированный геттер.
- Если хоть одного нет →
inspector.recordSkip('missing-required'), обработчик не вызывается. - Если все есть → обработчик запускается как обычно.
required — это шлюз, позволяющий безопасно собирать сценарии. Если <UserProvider> ещё не размещён, никто ничего не запускает; как только он смонтируется, следующее событие запустит обработчик.
schedule (опционально, по умолчанию 'microtask')
Заголовок раздела «schedule (опционально, по умолчанию 'microtask')»Управляет тем, когда отправленное событие диспатчится обработчикам.
| Значение | Поведение | Сценарий |
|---|---|---|
'microtask' (по умолчанию) | Все события, отправленные в одном тике, батчатся и диспатчатся в следующем microtask. | Почти всё. Хорошо сочетается с React. |
'sync' | Диспатч сразу, до возврата из fireEvent. | Тесты, горячие пути, где надо проверить побочный эффект в том же кадре вызова. |
Будущие планировщики ('animation-frame', 'idle', 'priority') есть в roadmap как опциональные варианты, а не как умолчания.
concurrency (опционально, по умолчанию 'take-latest')
Заголовок раздела «concurrency (опционально, по умолчанию 'take-latest')»Управляет поведением async обработчиков, когда триггер срабатывает снова, пока предыдущий прогон ещё в полёте.
| Стратегия | Поведение |
|---|---|
'take-latest' (по умолчанию) | Новый прогон отменяет предыдущий через signal.aborted = true. За проверку отвечает сам предыдущий прогон. |
'take-every' | Каждый прогон идёт независимо. Без отмены. |
'take-first' / 'exhaust' | Новые срабатывания игнорируются, пока один в полёте. |
'queue' | Новые срабатывания встают в очередь; сериализуются. |
'sync' | То же, что take-every; нужен как маркер для документации синхронных обработчиков. |
Это важно только для async обработчиков. У синхронных обработчиков нет понятия «в полёте». См. Стратегии конкурентности.
scope (опционально)
Заголовок раздела «scope (опционально)»Привязывает триггер к <TriggerScope id="...">. Внутри этого скоупа триггеру видны только регистрации, сделанные через useCondition / useAction в том же скоупе. Без скоупа триггер видит все глобальные регистрации.
Сценарий — изоляция фичи: два экземпляра одной триггер-логики работают в двух разных панелях чата, у каждого свой стейт, и их условия не наступают друг другу на пятки. См. Скоупы.
handler (обязательно)
Заголовок раздела «handler (обязательно)»Функция, которая запускается, когда происходит подходящее событие и все required-условия на месте. Аргумент — контекст триггера (ctx) с шестью полями:
Полный разбор каждого поля — в Обработчиках.
Обработчик может быть async. Тогда он получает AbortSignal, который рантайм переключает, когда новый прогон вытесняет этот (под take-latest) или когда рантайм / скоуп уничтожается. Передавай его в свои fetch-вызовы.
Что возвращает createTrigger
Заголовок раздела «Что возвращает createTrigger»Объект Trigger<Schema> с небольшим API:
К большинству из этого ты никогда не прикоснёшься — dispose нужен в тестах, disable — для фича-флагов.
Где живёт триггер
Заголовок раздела «Где живёт триггер»Договорённость — один триггер на файл, с суффиксом .trigger.ts. Помести его в src/triggers/ или положи рядом с фичей, к которой он относится:
Автообнаружение через @triggery/vite подхватывает каждый *.trigger.ts и импортирует его на старте. С ним руками никогда не приходится писать import './triggers/message.trigger'.