Рантайм
Runtime — это единственный объект, который владеет всем, что «живо» в Triggery: реестром триггеров, индексом событий, планировщиками, цепочкой middleware, кольцевым буфером инспектора и контроллерами незавершённых async-прогонов.
Обычно создаётся ровно один рантайм на приложение. Можно создать больше — для тестов, для микрофронтендов, для мультитенант-изоляции — и каждый будет независимым.
createRuntime(options?)
Заголовок раздела «createRuntime(options?)»Каждый параметр опционален. Умолчания подобраны под распространённый сценарий:
| Параметр | По умолчанию | Что делает |
|---|---|---|
middleware | [] | Массив Middleware объектов, применяемых к каждому триггеру. |
maxCascadeDepth | 3 | Насколько глубоко может рекурсивно расходиться кросс-триггерный fanout, пока рантайм не выдаст onCascade({ kind: 'overflow' }). |
inspectorBufferSize | 50 | Размер кольцевого буфера инспектора. Игнорируется при отключённом инспекторе. |
inspector | undefined (auto: DEV on, PROD off) | true / false / { dev?, prod? }. Управляет аллокацией снимка на прогон и payload’ом subscribe(). |
Стоит подчеркнуть умолчания инспектора: в DEV снимки есть, в PROD — нет. С выключенным инспектором горячий путь полностью пропускает аллокацию снимка на прогон — это ~30-40% дополнительной пропускной способности диспатча. Мосты типа @triggery/devtools-redux и React-хук useInspectHistory детектят выключенный инспектор и выдают одноразовый DEV-варн, если ты забыл его включить.
createRuntime возвращает объект Runtime:
Большая часть полей — наблюдательные; из кода приложения тебе пригодятся fire, fireSync, subscribe, getInspectorBuffer и dispose.
Рантайм по умолчанию
Заголовок раздела «Рантайм по умолчанию»Если ты нигде явно не передаёшь рантайм, Triggery использует ленивый глобальный синглтон:
В рантайм по умолчанию createTrigger(config) регистрирует триггер, если рантайм не передан. Туда же фреймворковые провайдеры падают, если ты разместил компоненты без <TriggerRuntimeProvider> — удобно для прототипов, менее идеально для тестов и SSR.
Для приложений с одним рантаймом игнорируй умолчание и пробрасывай рантайм явно через провайдер. Для библиотек принимай аргумент-рантайм или фолбэчься на getDefaultRuntime(), чтобы потребители могли переопределять.
Подключение через провайдер
Заголовок раздела «Подключение через провайдер»Каждый биндинг поставляет компонент-провайдер, публикующий рантайм для дочерних хуков:
Хуки разрешают рантайм, поднимаясь вверх по дереву: побеждает ближайший провайдер; если ни одного не смонтировано — используется рантайм по умолчанию. Этот фолбэк существует, чтобы один забытый провайдер не сломал приложение — вместо этого ты увидишь, как все твои хуки общаются с глобальным умолчанием, что скорее всего и было нужно.
Когда создавать больше одного рантайма
Заголовок раздела «Когда создавать больше одного рантайма»Несколько ситуаций оправдывают дополнительные рантаймы:
Тесты. Каждому тесту — свой рантайм, чтобы триггеры, условия и действия, зарегистрированные в одном тесте, не утекали в следующий. @triggery/testing поставляет createTestRuntime(), который оборачивает createRuntime тестовым планировщиком, удобным для синхронных ассертов.
Микрофронтенды. Два независимо поставляемых React-приложения, смонтированные на одну страницу, по умолчанию не должны делить реестр триггеров — они могут определять один и тот же triggerId с разными схемами или предполагать разный middleware. Каждый MFE создаёт свой рантайм; хост-страница решает, нужно ли мостить события между ними через top-level middleware.
Мультитенант. SaaS-дашборд, гоняющий несколько тенант-песочниц рядом, даёт каждой свой рантайм. Триггеры, условия и записи инспектора остаются изолированными; debug-панель тенанта видит только его прогоны.
SSR и RSC. На сервере важна изоляция на запрос. Создавай рантайм на запрос, монтируй React-дерево против него, уничтожай в конце. См. Server-side rendering.
fire vs fireSync
Заголовок раздела «fire vs fireSync»Оба отправляют событие через индекс событий рантайма. Разница — в планировании:
runtime.fire(name, payload)— уважаетscheduleтриггера ('microtask'по умолчанию). Вызов возвращается сразу; обработчики запускаются в будущем тике.runtime.fireSync(name, payload)— полностью обходит планировщик триггера; обработчик запускается до возврата вызова.
Используй fire почти везде. fireSync — в тестах и внутри узких бенчмарков, где нужно проверить побочный эффект в том же кадре вызова. В продакшене он редко нужен — microtask-планировщик дёшев и страхует от ловушек рендер-цикла.
subscribe и буфер инспектора
Заголовок раздела «subscribe и буфер инспектора»subscribe регистрирует слушатель, который получает каждый TriggerInspectSnapshot, записываемый инспектором:
getInspectorBuffer() возвращает последние N снимков (inspectorBufferSize), новейшие сначала. Оба метода — no-op, когда инспектор выключен.
Это субстрат, на котором сидит каждая devtools-панель. React-хук useInspectHistory(limit) — тонкая обёртка поверх: он subscribe-ится, слайсит буфер и перерендеривается. См. Инспектор.
Middleware
Заголовок раздела «Middleware»runtime.use(...) в V1 API нет — middleware задаётся при создании и неизменен на время жизни рантайма. Причина — производительность: горячий путь диспатча кеширует флаги hasMiddleware и trackTiming один раз, потом полностью пропускает целые ветви, когда middleware нет. Мутация middleware на лету это бы инвалидировала.
Для динамического включения / выключения — пиши middleware со своим переключателем:
Позднее подключение лежит в роадмапе V1.1, как только стоимость рантайма будет отбенчмаркана против твоих приложений. См. Middleware для полного списка хуков (onFire, onBeforeMatch, onSkip, onActionStart, onActionEnd, onError, onCascade).
Безопасность каскадов
Заголовок раздела «Безопасность каскадов»Каскад — это когда действие или обработчик отправляет ещё одно событие. Есть два риска:
- Неограниченная глубина — спирали
A → B → C → D → …. Рантайм останавливается наmaxCascadeDepth(по умолчанию3) и выдаётonCascade({ kind: 'overflow' }). - Циклы —
A → B → A. Рантайм идёт по цепочке родительских диспатчей (это linked-list, не set, поэтому без аллокаций) и пропускает виноватый вызов сonCascade({ kind: 'cycle' }).
Поднимай лимит глубины, когда твой домен реально этого требует:
Но: подумай дважды перед поднятием. Глубокие каскады почти всегда — признак того, что правило надо разбить на меньший граф, а не на более глубокий.
См. Каскад.
Осмотр графа
Заголовок раздела «Осмотр графа»runtime.graph() возвращает JSON-дружественный снимок зарегистрированных триггеров и индекса событий — форма стабильная, безопасно логировать или слать через postMessage:
CLI-команда triggery graph использует это для отрисовки диаграммы зависимостей: какие события питают какие триггеры по всему приложению. Devtools-панель использует это для отрисовки дерева реестра.
Disposal
Заголовок раздела «Disposal»runtime.dispose() — это graceful shutdown:
- Каждый незавершённый async-прогон прерывается через свой
AbortController. - Каждый ожидающий таймер debounce / defer чистится.
- Реестр триггеров очищается; индекс событий очищается.
- Буфер инспектора очищается.
Существующие ссылки на объекты Trigger продолжают существовать как JS-значения — они просто больше не привязаны к рантайму. Вызов .fire(...) после dispose() — no-op (в индексе событий нет совпадений).
Для SSR с изоляцией на запрос dispose() в конце обработки запроса — правильный вызов. Для тестов — в afterEach. Для долгоживущих приложений никогда не зовётся.
Заметки о жизненном цикле
Заголовок раздела «Заметки о жизненном цикле»Dev-режим. Инспектор включён, last-write-wins коллизии выдают одно предупреждение на пару (triggerId, name), scope-мисматчи — одно предупреждение. Ни одно из них не ошибка, это сигналы, которые читаешь в DevTools.
React StrictMode. Цикл mount → cleanup → mount безопасен. Каждая (trigger, name) пара имеет один слот; cleanup первого монтирования удаляет запись (она ещё live на тот момент), второй монт записывает заново. Stale unregister-токены (если регистрация уже была перезаписана позже) — no-op.
SSR. Создавай один рантайм на запрос, монти своё дерево против него, дожидайся незавершённых async-прогонов (если есть), уничтожай в конце. См. Server-side rendering для истории про стриминг / гидрацию.
Тест-режим. Сочетай createTestRuntime (из @triggery/testing) с fireSync для полностью синхронных ассертов. Тестовый планировщик продвигается по требованию для ассертов на microtask. См. Модульные тесты.