Каскады
Каскад — это вызов, происходящий внутри работающего обработчика. Тело useAction зовёт runtime.fire('next-event'); это событие, в свою очередь, матчится с другим триггером; обработчик того триггера запускается; и так далее. Каскады позволяют сценариям распространяться поперёк фичей так, что продьюсеру второго события не нужно знать о продьюсере первого.
Они также позволяют построить бесконечный цикл в три строки, так что рантайм поставляется с двумя ремнями безопасности по умолчанию — лимит глубины и per-fire проверка циклов — плюс хук middleware, чтобы наблюдать всё, что проходит.
Анатомия каскада
Заголовок раздела «Анатомия каскада»Ниже — цепочка, которую Triggery реально трекает. A запускает top-level событие; его обработчик зовёт действие; это действие — или сам обработчик — зовёт runtime.fire, эмитя новое событие; триггер B подписан на это событие и запускается; и так далее.
Бухгалтерия рантайма — в meta:
Те же поля попадают в снепшоте инспектора — это и питает каскадное дерево в DevTools.
Дефолтная безопасность: maxCascadeDepth
Заголовок раздела «Дефолтная безопасность: maxCascadeDepth»createRuntime дефолтит maxCascadeDepth в 3. Всё, что за этим — пропускается; рантайм эмитит onCascade с kind: 'overflow', и диспатч этой цепочки останавливается. По умолчанию исключение не кидается.
Число маленькое намеренно. Три шага — этого достаточно для легитимных кейсов (“событие → обработчик → каскад-событие → обработчик → каскад-событие → обработчик”). Четыре и больше обычно значит, что кто-то прикрутил мелкий пайплайн к реестру триггеров вместо того, чтобы написать функцию. Поднимать лимит — запах, если у тебя нет письменной причины.
Как выглядит overflow
Заголовок раздела «Как выглядит overflow»Middleware onCascade происходит с полным контекстом. Без установленного middleware overflow тихий на рантайме, но всё ещё аннотирован в буфере инспектора (status 'skipped', reason cascade-overflow).
Детект циклов
Заголовок раздела «Детект циклов»Независимо от глубины, рантайм детектит циклы per top-level вызов. Пока обработчик идёт, диспетчер держит ссылку на parent-цепочку; перед повторным входом в триггер он идёт по этой цепочке, ища id триггера. Хит значит “этот триггер уже работает выше в цепочке” — второй вход пропускается с kind: 'cycle'.
Детект циклов per-top-level-fire — независимые top-level вызовы не делят цепочку. Проверка O(depth), а не O(триггеров-в-рантайме), так что ремень безопасности не стоит ничего на горячем пути, даже с тысячами зарегистрированных триггеров.
Конфигурация поведения каскадов
Заголовок раздела «Конфигурация поведения каскадов»Сегодня одна ручка — maxCascadeDepth — плюс хук middleware onCascade. Будущие opt-in режимы ('forbid', 'throw') в роадмапе.
Если хочешь строгого поведения сегодня — пиши middleware:
Throw приземлится в try/catch родительского обработчика (по Error handling) — он не пробрасывается в runtime.fire, который by design fire-and-forget.
DEV: запретить каскады вообще
Заголовок раздела «DEV: запретить каскады вообще»В редакторе предпочитай статическую проверку из ESLint-плагина. Лимит каскадов рантайма ловит то, что просочилось в рантайм; lint-правило держит каскады вне кодовой базы.
Это ловит самый частый каскад по статической структуре — реактор, запускающий другое событие. Рантайм держит лимит глубины сверху, потому что некоторые легитимные каскады не имеют формы useEvent (например, обработчик, зовущий runtime.fire напрямую).
Когда каскад в порядке
Заголовок раздела «Когда каскад в порядке»Каскады отрабатывают свой вес, когда каждый шаг независим и осмыслен сам по себе:
- Закрытие модалки → событие аналитики. Сценарий модалки не знает, что что-то это трекает. Сценарий аналитики не знает, какая модалка закрылась.
- Auth-refresh успешен → перевзведи watchers. Watcher’ы знают про “сессия изменилась”, не про “триггер refresh сделал изменение”.
- Документ открыт → обновление списка недавно просмотренных. Два сценария — открытие документа и учёт недавних — делят границу события.
В каждом кейсе родитель короткий, ребёнок короткий, и ни одному не нужно читать или звать другого.
Когда каскад — это запах
Заголовок раздела «Когда каскад — это запах»Цепочка перестаёт быть оркестрацией и начинает быть закодированной функцией в тот момент, когда один шаг существует только чтобы включить следующий.
- Три триггера, один исход.
validate → save → toast— это функция с обманчивым следом в реестре. Консолидируй (см. Борьба со спагетти). - Каскад, кормящий назад. Триггер A производит событие B; триггер B производит событие A; ты обнаруживаешь это через cycle-warning. Останови, найди третий триггер, владеющий правдой, и пусть оба зависят от него.
- Каскад через
setTimeoutилиawait. Каскад-трекинг следует только за синхронной частью async-обработчика — после первогоawaitследующийruntime.fire— это свежий top-level эмит. Если твой “каскад” работает только потому, что родитель sync, ты полагаешься на implementation detail; вытяни это в явный продьюсер.
Каскадное дерево в инспекторе
Заголовок раздела «Каскадное дерево в инспекторе»Когда инспектор включён (DEV по умолчанию, opt-in для PROD), каждый прогон несёт parentRunId / parentTriggerId в снепшоте. @triggery/devtools-redux и @triggery/devtools-bridge рендерят это как дерево:
Тот же вид можно собрать из runtime.getInspectorBuffer() за 30 строк в кастомной панели — см. Инспектор.
Чит-шит
Заголовок раздела «Чит-шит»| Хочешь… | Тянись к |
|---|---|
| Жёсткий потолок длины цепочки | createRuntime({ maxCascadeDepth }) |
| Наблюдать overflow’ы и циклы | Middleware.onCascade |
| Запретить каскады в редакторе | @triggery/no-event-cascade |
| Throw на каждое каскад-событие | Кастомный onCascade middleware, который re-throw’ит |
| Исследовать конкретную цепочку | meta.parentRunId + runtime.getInspectorBuffer() |