Обработчики
Обработчик — это функция, которую вызывает рантайм, когда отправленное событие совпадает с триггером и присутствуют все required условия. Он получает один аргумент — ctx — у которого шесть полей:
Обработчик — это обычный JavaScript. Никакого реактивного графа вокруг него; никакие прокси наружу не утекают. На каждый вызов выдаётся свежий ctx. Дальше — пробежка по каждому полю.
event — discriminated union
Заголовок раздела «event — discriminated union»event — это discriminated union по всем событиям, перечисленным в схеме. Форма — { readonly name: K; readonly payload: EventMap[K] } для каждого K. Разветвляй по event.name, чтобы сузить event.payload:
Если в триггере объявлено ровно одно событие, тип event.payload — это просто payload этого события, без switch.
conditions — типизированная карта геттеров
Заголовок раздела «conditions — типизированная карта геттеров»conditions — это зафиксированный снимок значений условий триггера, лениво читаемых при первом обращении. Каждый элемент — T | undefined, потому что регистрации может ещё не быть. Прокси кеширует на прогон — два чтения одного и того же условия дадут одно и то же значение.
Три вещи на запомнить:
- Порядок важен для стоимости, не для корректности. Рантайм зовёт геттер только тогда, когда ты его прочитал. Ставь дешёвые проверки впереди.
- Прочёл один раз, ветвись много.
const s = conditions.settings; if (!s) return; if (!s.notifications) return; if (s.dnd) return;— идиоматично. - Никакой распаковки через
.value. Что прочитал — то и есть значение, которое вернул геттер.
См. Условия для стороны регистрации.
actions — прокси с опциональными членами + цепочка таймеров
Заголовок раздела «actions — прокси с опциональными членами + цепочка таймеров»actions — это поверхность побочных эффектов. Каждое объявленное действие — ((payload) => void) | undefined. Плюс три композируемые обёртки:
debounce, throttle, defer возвращают новый прокси с такой же формой — композируемый, но плоский. Рантайм владеет состоянием таймеров на каждый триггер и отменяет их при dispose.
Действие с void-payload вызывается без аргумента: actions.beep?.().
См. Действия для стороны регистрации и модели таймеров.
check — типизированные предикаты над условиями
Заголовок раздела «check — типизированные предикаты над условиями»check — это маленький DSL, который делает три вещи, которые иначе пришлось бы писать руками: сужает T | undefined, гейтит на предикате и аккуратно возвращает false, если условие не зарегистрировано.
check.is(key, predicate)
Заголовок раздела «check.is(key, predicate)»True, если условие существует и предикат вернул правду. Предикат получает значение, типизированное как NonNullable<T>:
Это идиоматичное сокращение для «если X присутствует и X.y truthy». Ручная версия тоже нормально; check.is короче и красиво показывается в snapshotKeys инспектора.
check.all(map)
Заголовок раздела «check.all(map)»Каждое перечисленное условие должно существовать, и его предикат должен вернуть true. Ключи карты — имена условий, значения — предикаты:
Отсутствующее условие (без зарегистрированного геттера) приводит к тому, что all вернёт false — так же, как и непрошедший предикат. Режима «частичная правда» нет.
check.any(map)
Заголовок раздела «check.any(map)»Хотя бы одно из перечисленных условий должно существовать и пройти. Отсутствующие условия пропускаются (не считаются провалом):
any короткозамыкает на первом совпадении.
Когда что использовать
Заголовок раздела «Когда что использовать»- Одно условие, одна проверка →
check.is. - Несколько условий, все должны пройти →
check.all. - Несколько условий-escape-hatch, любое из которых подходит →
check.any. - Сложная комбинированная логика → ручной
if/elseс явным сужением —checkне пытается быть query language.
signal — abort-сигнал этого прогона
Заголовок раздела «signal — abort-сигнал этого прогона»signal — это AbortSignal, который рантайм переключает, когда:
- Новый прогон вытесняет этот (при
concurrency: 'take-latest', по умолчанию). - Рантайм уничтожается.
- Триггер разрегистрируется.
Для sync-обработчиков signal — формальность: они возвращаются до того, как может случиться вытеснение. Для async-обработчиков пробрасывай его в fetch / async-итерацию / event listener:
Сигнал помогает двумя способами:
fetch(url, { signal })— сетевой слой прервёт запрос, когда сигнал переключится. Никакого зря потраченного трафика.if (signal.aborted) return;после каждогоawait— защитно, чтобы медленный ответ не задиспатчил действия для уже устаревшего события.
При concurrency: 'queue' сигналы не переключаются на старте нового прогона — прогоны сериализуются. При take-every — тоже не переключаются. См. Стратегии параллелизма.
meta — идентичность прогона и инфо о каскаде
Заголовок раздела «meta — идентичность прогона и инфо о каскаде»meta несёт идентифицирующую информацию о прогоне:
Распространённое применение — структурированный логинг:
runId — тот же id, по которому инспектор ключует записи, так что серверный лог и таймлайн инспектора тривиально коррелируют. cascadeDepth и parentRunId — это то, что питает отрисовку каскадной цепочки в @triggery/devtools-redux.
Возвращаемое значение
Заголовок раздела «Возвращаемое значение»Обработчик может вернуть void или Promise<void>. Оба first-class:
Если обработчик кидает (или реджектит), рантайм ловит, помечает запись инспектора 'errored' и зовёт хук middleware onError. Триггер остаётся зарегистрированным — следующее событие отработает обработчик нормально.
Возврат чего-то отличного от undefined игнорируется — тип это запрещает. Обработчик — для побочных эффектов, не для вычислений.
Общие паттерны
Заголовок раздела «Общие паттерны»Ранний return при skip
Заголовок раздела «Ранний return при skip»Самая распространённая форма — guard, потом действие:
Читается сверху вниз как спецификация: «пропусти, если нет settings; пропусти, если тот же канал; пропусти, если notifications выключены; иначе — тост». Файл триггера — это спецификация.
Fan-out действий
Заголовок раздела «Fan-out действий»Несколько эффектов в одном прогоне:
Каждое действие независимо; инспектор записывает список executedActions на прогон — можно увидеть, какие именно побочные эффекты отработали.
Каскад — отправка другого события
Заголовок раздела «Каскад — отправка другого события»Достучаться до рантайма, чтобы отправить дочернее событие изнутри обработчика:
Рантайм помечает новый вызов как каскад и проверяет лимит глубины. См. Каскад.
Логинг с meta
Заголовок раздела «Логинг с meta»Для структурного трейсинга:
Те же поля попадают в инспектор, так что прод-логи и записи DEV-инспектора выстраиваются по runId.