Условия
Условие — это кусочек состояния мира, в который обработчик может захотеть заглянуть, когда запускается. Компонент-провайдер регистрирует геттер под именем условия; когда триггер срабатывает, рантайм один раз вызывает геттер и фиксирует значение на оставшееся время прогона.
Идея небольшая, но последствия у неё большие: триггер пуллит состояние, провайдер его не пушит. Никаких подписок, никаких перерендеров, никакого diff-трекинга. У Triggery нет реактивного графа — есть Map из функций () => T, которые вызываются ровно тогда, когда они нужны событию.
Объявляем условие
Заголовок раздела «Объявляем условие»Условия живут в карте conditions внутри схемы. Каждая запись отображает имя на тип значения, которое должен вернуть геттер:
Три имени, три типа значения. Зарегистрированы ли они на момент отправки — отдельный вопрос; см. шлюз required ниже.
Регистрируем геттер
Заголовок раздела «Регистрируем геттер»Провайдеры регистрируют геттеры через хук биндинга. useCondition принимает триггер, имя условия, функцию-геттер и (в React) опциональный массив зависимостей с такой же семантикой, как у useMemo:
Массив зависимостей работает ровно как у useCallback / useMemo: при изменении любой зависимости рантайм перечитает свежую замыкающую функцию. Сам геттер обёрнут в стабильный ref, поэтому перерендеры не перерегистрируют условие в рантайме.
Pull-only — что это значит на практике
Заголовок раздела «Pull-only — что это значит на практике»Геттер запускается только в момент отправки события. Конкретно:
- Между состоянием провайдера и рантаймом не настраиваются никакие подписки.
- Перерендеры провайдера не зовут геттер, не инвалидируют ничего, не уведомляют другие компоненты.
- Триггер, который никогда не срабатывает, никогда не запросит значение. Дёшево.
- Один и тот же обработчик, читающий
conditions.settingsдважды за прогон, увидит одно и то же значение. Прокси кеширует на прогон.
Это и есть то свойство, которое отвязывает триггер от React-цикла рендеров. Слой тостов, панель настроек и список чатов могут жить рядом с триггером — и ни один из них не отрендерится, когда придёт сообщение.
Читаем условия внутри обработчика
Заголовок раздела «Читаем условия внутри обработчика»Каждое условие, к которому может обратиться обработчик, типизировано как T | undefined. Причина честная: в imperative форме createTrigger({...}) TypeScript не умеет сужать тип на основе required: [...]. Поэтому есть три безопасных способа прочитать внутри imperative-handler’а (либо используй builder API из @triggery/core/builder для автоматического сужения):
Используй поле required для случаев «обработчик бессмыслен без этого». Используй check.is для «обработчик должен пропуститься, если X не true». Они компонуются.
Шлюз required
Заголовок раздела «Шлюз required»У условия, перечисленного в required: [...], должен быть хотя бы один зарегистрированный геттер в момент отправки, иначе обработчик будет пропущен до запуска. Инспектор запишет запись 'skipped' с reason 'missing-required-condition:<name>'.
Это и есть тот шлюз, который позволяет безопасно собирать сценарии. Если <UserProvider> ещё не смонтирован, события 'new-message' приходят, получают записанный skip — и пользователь ничего не видит. Как только провайдер смонтируется, следующее событие отработает обработчик нормально. Провайдер может лежать в другой папке фичи, не в той, где триггер; триггер его не импортирует.
В инспекторе это выглядит так:
— точный сигнал, что что-то монтируется поздно или не монтируется вовсе.
См. Анатомия триггера → required для полной механики.
Паттерн адаптера — обёртка над внешними сторами
Заголовок раздела «Паттерн адаптера — обёртка над внешними сторами»Значение из useState — это тривиальный геттер. Тот же паттерн работает для любого внешнего стора — Zustand, Redux, Jotai, MobX, Signals, TanStack Query. Пакеты-адаптеры делают это в ~30 строк: они синхронно читают из стора и отдают значение через useCondition.
Стор Zustand перерендеривает провайдер, когда его слайс меняется — но это уже дело провайдера, Triggery до этого нет дела. Рантайм всё равно вызовет () => s.id только в момент отправки, а закешированный селектор useStore отдаст последнее значение.
Та же форма работает для @triggery/redux, @triggery/jotai, @triggery/query и любого стора, который ты напишешь сам — см. Адаптеры.
Владение по правилу last-write-wins
Заголовок раздела «Владение по правилу last-write-wins»Что если два провайдера регистрируют одно и то же имя условия на одном и том же триггере? Каждая пара (trigger, name) имеет один слот: самая свежая регистрация перезаписывает предыдущую. Когда она затем размонтируется, слот очищается (предыдущее значение не восстанавливается — v0.10 убрала stack-based fallback).
Два намеренных следствия:
- Тесты и оверрайды просты. Тест монтирует провайдер с тестовым значением вторым — оно побеждает.
- StrictMode безопасен. React 18 StrictMode в dev монтирует → размонтирует → монтирует. Cleanup первого монтирования очищает слот ещё до того, как второй монт запишет — никаких ложных коллизий.
В DEV рантайм выдаёт один console.warn на пару (triggerId, conditionName), когда приходит вторая живая регистрация:
Это сигнал, что возможно что-то не так. Если last-write-wins тебе нужен по делу (оверрайды, тесты) — игнорируй смело. См. Владение для полного обсуждения и паттернов композиции значений из нескольких источников.
Условия в скоупах
Заголовок раздела «Условия в скоупах»По умолчанию условия регистрируются глобально. Оберни поддерево в <TriggerScope id="...">, и условия, зарегистрированные внутри, станут видны только тем триггерам, у которых scope совпадает:
Два <ChatRoom> в двух скоупах работают с двумя независимыми условиями settings. Ключ скоупа — строка; матчить можно по тенанту, по роуту, по фиче-флагу. См. Скоупы.
Общие паттерны
Заголовок раздела «Общие паттерны»Условия — это существительные, а не действия. settings, currentUserId, cartTotal — не getSettings или loadCart. Глагол неявно подразумевается в «прочитай это в момент отправки».
Одно условие на одно понятие. Не упаковывай несвязанные значения в одно условие appState только потому, что они лежат в одном Zustand-сторе. Рантайму важно, зарегистрировано ли значение; смешение понятий делает список required нечестным.
Геттеры держи дешёвыми. Они запускаются в момент отправки, и плохо ведущий себя геттер кинет исключение прямо в диспатч-петле. Не делай fetch из геттера. Не разжимай синхронно 3 МБ блоб. Если значение дорогое — мемоизуй его на уровне провайдера, а геттер пусть возвращает закешированный результат.
Используй check.is вместо вложенных if. Короче, сам обрабатывает undefined-случай, а инспектор пишет запись в snapshotKeys только если условие реально читалось. Чище трейсы.