Стратегии параллелизма
У async-обработчиков есть длительность. Интересный вопрос — что произойдёт, когда тот же триггер произойдёт ещё раз, пока предыдущий прогон ещё в полёте. В Triggery пять реальных стратегий (плюс маркер sync для документации), задаваемых в конфиге триггера:
По умолчанию 'take-latest' — правильный выбор для поиска, автокомплита, навигации и большинства user-facing реактивных потоков. Остальные четыре существуют, потому что каждая по-своему правильна для узких повторяющихся кейсов.
Пять стратегий + маркер sync
Заголовок раздела «Пять стратегий + маркер sync»| Стратегия | Поведение, когда приходит новое событие посреди прогона | Лучше всего подходит для |
|---|---|---|
'take-latest' (по умолчанию) | Прервать предыдущий прогон (signal.aborted = true, reason 'superseded-by-latest'). Новый прогон стартует сразу. | Поиск / автокомплит, загрузки навигации, всё, где важен только последний ответ. |
'take-every' | Оба прогона идут независимо. Никакого abort, никакого skip. | Аналитика, логинг, fire-and-forget побочные эффекты, которые не должны мешать друг другу. |
'take-first' | Пока прогон в полёте, новые события сбрасываются (записываются как skipped, reason concurrency-take-first). | Идемпотентные дорогие чтения, где результат в полёте удовлетворит вызывающих. |
'exhaust' | На проводе идентично 'take-first': новые события сбрасываются, пока текущий не завершится. | То же самое; выбирай то имя, которое лучше читается в конфиге. |
'queue' | Новые прогоны ждут своей очереди. Каждый стартует только после завершения предыдущего. | Мутации / записи, где важен порядок: PATCH/POST/PUT к одному ресурсу. |
'sync' | Документационный маркер для sync-only обработчиков. На рантайме идентичен 'take-every'. | Чисто синхронные обработчики; сигналит намерение читателям. |
Выбор стратегии
Заголовок раздела «Выбор стратегии»Реальные выборы, выведенные из формы сценария:
Сквозная мысль через все пять: тело обработчика идентично. Стратегия — это одна строка конфига, никогда не код.
Как задать стратегию
Заголовок раздела «Как задать стратегию»Per-trigger, в конфиге:
Глобального дефолта на весь рантайм нет — выбор локален для сценария, by design. Если хочется “все писатели в этой фиче используют queue”, это намёк, что файл триггера стоит держать в одном месте, а не заводить глобальный дефолт.
Взаимодействие с прокси действий
Заголовок раздела «Взаимодействие с прокси действий»concurrency и actions.debounce / throttle / defer работают на разных уровнях. Не путай их:
| Механизм | Гранулярность | Эффект |
|---|---|---|
concurrency | Весь прогон обработчика | Когда два события пересекаются, что рантайму делать с прогоном в полёте? |
actions.debounce(800).foo() | Один вызов действия | Запланировать этот единичный вызов на вызов через 800 мс после последнего вызова, заменив любой ожидающий с тем же ключом. |
actions.throttle(2000).foo() | Один вызов действия | Leading-edge throttle: вызов сразу, игнор последующих вызовов в течение 2 с. |
actions.defer(50).foo() | Один вызов действия | Вызов ровно через 50 мс; новые вызовы его не заменяют. |
Можно свободно мешать. Типичный паттерн: обработчик take-latest + actions.debounce(80).showResults, чтобы схлопнуть два соседних рендера.
См. Debounce и throttle для полного справочника прокси.
Визуализация таймлайнов
Заголовок раздела «Визуализация таймлайнов»Ниже — каждая строка одна стратегия. События приходят в t=0мс, t=100мс, t=200мс. Каждый вызов обработчика занимает 250 мс.
Заметки к чтению:
take-latest— предыдущий прогон получаетsignal.aborted = trueв момент выпуска следующего события. Любой последующийsignal.throwIfAborted()в abort’нутом обработчике короткозамкнётся; любой ожидающийfetch(..., { signal })зареджектитсяAbortError.queue— старт B гейтится резолвом A. Общее wall-time растёт линейно. Если нужна ограниченная concurrency выше 1, пиши кастомное решение снаружи Triggery (worker pool, p-limit и т.п.) и зови его изtake-everyобработчика.take-every— три пересекающихся прогона, каждый владеет своимsignal. Abort не происходит, пока не диспознут рантайм.
Смотрим причины abort в DEV
Заголовок раздела «Смотрим причины abort в DEV»Каждый прерванный / пропущенный прогон записывается в кольцевой буфер инспектора со стабильной строкой reason. Полезно, когда сценарий “не происходит” и ты хочешь знать почему.
| Reason | Смысл |
|---|---|
'superseded-by-latest' | take-latest прервал предыдущий прогон, потому что произошло более новое событие. |
'concurrency-take-first' | Новое событие дропнули, потому что что-то в полёте. |
'concurrency-exhaust' | То же самое, записано под именем exhaust. |
'disposed' | Рантайм, скоуп или триггер были диспознуты посреди прогона. |
'hmr' | Vite/webpack HMR переоценил модуль триггера; предыдущий экземпляр был диспознут. |
Читай буфер откуда угодно:
Или в React — отрендерь список через useInspectHistory(trigger) из @triggery/react. DevTools-мост сериализует те же снепшоты через postMessage.
Распространённая ловушка: незакрытые писатели предыдущего прогона
Заголовок раздела «Распространённая ловушка: незакрытые писатели предыдущего прогона»take-latest прерывает обработчик, а не уже задиспатченные побочные эффекты. Если предыдущий прогон уже позвал actions.show?.(...) с частичными данными до точки abort’а, это состояние уже в твоём сторе.
Если пользователь кликнул на A, потом на B, до прибытия orgs-ответа, стор окажется с профилем A и всем остальным B. Фикс зависит от того, что ты хочешь:
- Всё-или-ничего: собирай в локальные переменные, диспатчи все действия только после последнего
await. (См. Асинхронные обработчики → Последовательные await’ы.) - Версионирование по ключу: включай
event.payload.idв каждый payload действия и пусть реакторы выкидывают устаревшие записи, если id больше не совпадает с текущим контекстом. - Сменить стратегию:
queueделает записи последовательными. Пользователь видит, что A завершилось, прежде чем стартует B.
Правильный ответ — зависит от сценария. Неправильный — делать вид, что проблемы нет, потому что take-latest “отменил” предыдущий прогон.
Распространённая ловушка: использовать queue для дедупликации
Заголовок раздела «Распространённая ловушка: использовать queue для дедупликации»queue не дедуплицирует. Три быстрых клика на кнопку “save” под queue производят три последовательных PATCH’а. Если нужно “максимум один ожидающий save”:
Или дебаунси действие, которое запускает событие:
queue — для порядка, не для throttle.