Области видимости
Скоуп — это строка-id, которую ты пришпиливаешь к части дерева компонентов. Внутри этого поддерева условия и действия регистрируются в приватный бакет. Триггер, объявленный с тем же scope, видит только этот бакет — он не видит регистраций, сделанных где-то ещё, и другие триггеры не видят регистраций внутри.
Вот и всё. Скоупы — самый маленький инструмент, который Triggery даёт для “мне нужно два экземпляра одного сценария без взаимного топтания”.
Когда тянешься к скоупу
Заголовок раздела «Когда тянешься к скоупу»Классический кейс: ты рендеришь ту же фичу N раз параллельно, и у каждого экземпляра — своё состояние. Три чат-панели в productivity-приложении. Пять воркспейс-табов в дизайнер-туле. Два модальных стека один над другим. С одним рантаймом и без скоупа каждое условие, зарегистрированное чат-панелью, видно каждому chat-notification-триггеру — включая экземпляр триггера, обрабатывающий сообщения другой панели. Ты в итоге запускаешь не тот тост на не той вкладке.
Каждый <ChatPanel> регистрирует свои условия в своём скоупе, а триггер уведомлений внутри той же панели видит только её представление мира.
Две половины контракта
Заголовок раздела «Две половины контракта»Скоуп работает, только когда обе стороны opt-in:
- Триггер объявляет
scope: <id>в конфигеcreateTrigger. - Компоненты, монтирующие условия, действия и (опционально) продьюсер события, живут под
<TriggerScope id={<id>}>.
Правила видимости
Заголовок раздела «Правила видимости»Матчинг строгий, специально.
scope триггера | Скоуп регистрации | Видно? |
|---|---|---|
'chat' | 'chat' | да |
'chat' | 'panel:general' | нет |
'chat' | глобально (без обёртки <TriggerScope>) | нет |
(без scope) | 'chat' | нет |
(без scope) | глобально (без обёртки <TriggerScope>) | да |
Триггер без scope — это глобальный триггер. Глобальные триггеры видят только глобальные регистрации. Скоупированные триггеры видят только регистрации с совпадающим скоупом. Никакого неявного fall-through из скоупа в глобал, никакого неявного broadcast из глобала в скоупы — обе половины явно opt-in.
В DEV scope-мисматчи репортятся одноразовым варном на регистрацию, чтобы проблема проводки не падала молча:
Если ты это видишь — одна из двух половин пропала. Самый частый вариант — “я добавил scope: в триггер, но забыл обернуть место использования”, или наоборот.
Стэкинг и вложенность
Заголовок раздела «Стэкинг и вложенность»<TriggerScope> можно вкладывать. Внутренний скоуп побеждает — композиции нет. С точки зрения реестра в точке, где запускается useCondition / useAction, активен только один id скоупа.
Это намеренно. Композиция (“регистрация в inner видна и триггеру, скоупированному в outer”) пробовалась на этапе дизайна и быстро перестала быть полезной — в момент, когда два родительских скоупа активны одновременно, тебе нужно правило приоритета, а наименее удивляющее правило было не делать этого. Триггеры и скоупы — 1:1 строки.
Если по-настоящему нужны пересекающиеся скоупы — дай каждому триггеру свой. Два триггера могут делить реализацию обработчика:
max-handler-size и остальное из ESLint-плагина по-прежнему применяется к общему обработчику — они видят тело функции, а не место вызова.
Disposal
Заголовок раздела «Disposal»Когда <TriggerScope> размонтируется, каждый useCondition / useAction внутри тоже размонтируется — стандартный путь React effect cleanup. Регистрации удаляются из стеков рантайма. Если триггер бежал с in-flight async-обработчиком, abort signal не флипается только анмаунтом скоупа — только supersession в take-latest или runtime.dispose(). Defensive-обработчики проверяют signal.aborted после каждого await и выходят рано. См. Concurrency для полного разбора.
Сам объект триггера не скоупируется в <TriggerScope>. Триггеры живут на время жизни рантайма (или до trigger.dispose()). С анмаунтом скоупа уходит бакет условий и действий, привязанных к этому id скоупа.
Скоупы vs. несколько рантаймов
Заголовок раздела «Скоупы vs. несколько рантаймов»<TriggerRuntimeProvider> — это более тяжёлый брат <TriggerScope>. Оба создают изоляцию; они живут на разных слоях.
| Concern | <TriggerScope> | Отдельный createRuntime() |
|---|---|---|
| Реестр триггеров | общий (одна карта на рантайм) | отдельные карты |
| Кольцевой буфер инспектора | общий | отдельный, конфигурируемый per-runtime |
| Стек middleware | общий (один на рантайм) | независимые стеки |
maxCascadeDepth, планировщик | общий | per-runtime override |
| Стоимость проводки | один prop на место использования | один провайдер, одна корневая проводка |
| Use case | параллельные экземпляры фичи, панели, табы | тесты, мультитенант, sandbox’ы микрофронтендов |
Правило большого пальца: тянись к скоупу, когда сценарии хотят быть сиблингами. Тянись к рантайму, когда правила движка хотят быть сиблингами (другой middleware, другая ёмкость инспектора, другая глубина каскадов, другой дефолтный schedule).
Тест обычно хочет свежий рантайм — см. Модульные тесты — потому что хочет, чтобы буфер инспектора стартовал пустым, и стек middleware был собственным для теста.
Разбор примера: уведомления в мульти-панельном приложении
Заголовок раздела «Разбор примера: уведомления в мульти-панельном приложении»Ниже — полная проводка для сценария чат-панелей из начала страницы. У каждой панели свой id скоупа, у каждой — свой активный канал, и три панели никогда не видят событий друг друга.
Три панели, три скоупа — каждый скоуп со своим бакетом условий/действий. Каждый экземпляр <ChatPanel> получает свои тосты; смена активного канала в одной панели не глушит уведомления в других.
Если в следующем квартале продуктовая команда захочет четвёртую панель с отдельным middleware и инспектором (скажем, тщательно трассируемую “support agent” панель, где хочется логать каждый вызов), повысь эту панель до собственного createRuntime() — три других сохранят свои скоупы.
Ограничения и подводные камни
Заголовок раздела «Ограничения и подводные камни»- Один id скоупа за раз на поддерево. Вложенные скоупы заменяют, мерджа нет.
- Кросс-скоупные вызовы — top-level. Скоупированный триггер, зовущий
runtime.fire('x'), не пробрасывает скоуп через новое событие — каскад-ребёнок матчится по своему полюscopeрантаймом. Если хочешь, чтобы один скоуп говорил с другим — полеscopeпринимающего триггера определяет видимость. - DEV-варн — per-
(label, triggerId, scope, name)— после того как ты его увидел для одной коллизии, та же проводка не будет спамить консоль на re-render’ах. - SSR: id скоупов — стабильные строки, дрейфа гидрации нет. См. Server-side rendering, если скоупы выводятся из request data.