PR в effector
deffector работает с effector в его нынешнем виде (22.6+) — на обходных путях. Шесть небольших изменений в самом effector сделали бы эти обходы ненужными, а данные — точнее. Изменения реализованы и проверены как ветки от master 93dd907 (effector 23.4.4).
Шесть изменений
| # | Изменение | Суть | Issues | Обход в deffector сейчас |
|---|---|---|---|---|
| 1 | Инспектор не ломает ядро | try/catch вокруг вызова инспектора в ядре | — | агент оборачивает свой колбэк в try/catch |
| 2 | Номера сообщений и транзакций | seq, parentSeq, launchId, filtered, derived в сообщениях | #977 | собственный счётчик, WeakMap<Stack, step> через setInspector |
| 3 | Все scope и их имена | inspect({ scope: 'all' }), fork({ name }), объявления scope | #659, #911, #1144 | stack.scope из setInspector, имена через registerScope |
| 4 | Причинность через границы | Message.cause для эффектов и вложенных вызовов | #1324, #541, #771 | stack.meta.fxID и эвристика «внешняя транзакция продолжилась» |
| 5 | Рёбра и удаление юнитов | inspectGraph({ links: true }): link (с reads) и dispose | #91, #1200, PR #1334, #1353 | обход node.next, family, шагов mov из store |
| 6 | Бюджет шагов (по желанию) | лимит шагов на корневой launch, ошибка с путём цикла | #331, #1153 | статический поиск циклов и аварийная остановка |
1. Инспектор не ломает ядро
Исправление бага, отправить первым. Ядро вызывает инспектор вне tryRun; исключение из инспектора раскручивает launch мимо восстановления состояния, isRoot навсегда остаётся false, и все последующие отложенные запуски выходят раньше времени — await fx() не завершается, fx.pending застревает в true. Патч оборачивает вызов в try { … } catch (err) { console.error(err) }. Два теста, оба падают без исправления.
2. Номера сообщений и транзакций
seq (сквозной номер сообщения), parentSeq (seq сообщения stack.parent), launchId (номер корневого launch), filtered (узел выполнен, но ветка остановлена) и уже существующий derived в типах. parentSeq стоит O(1), в отличие от trace: true, который добавляет 85% ко времени события. В реализации шаг, запустивший другие (watcher вызвал событие), получает номер раньше них, потому что ядро выполняет вложенный launch сразу; сортировка по seq даёт причинный порядок.
3. Все scope и их имена
inspect({ scope: 'all', fn }); // Message.scope: { id: string; name?: string } | null
fork({ name: 'client' });
inspectGraph({ fn }); // { type: 'scope', id, name } при fork, { type: 'scope-dispose', id }Сейчас фильтр scope — точное совпадение, перечислить scope нельзя. scope: null в сообщении означает вызов вне scope — ровно то, что нужно проверке «вызов вне scope» (#1144).
4. Причинность через границы
Message.cause — { kind: 'effect', fxID, callSeq } или { kind: 'nested', seq }. Раннер эффекта и onSettled запускаются без стека, поэтому трейс там обрывается; событие из watcher-а начинает новый корень. С cause воспроизведение не дублирует вложенные вызовы, а «unit call from pure function» может указать на источник (#1324). В реализации часть про эффекты целиком считается в inspect.ts; ядро отдаёт только stack.caller.
5. Рёбра и удаление юнитов
inspectGraph({ links: true }) сообщает { type: 'link', id, op, from, to, reads, region, loc } и { type: 'dispose', id }. За основу взят формат link из PR #1334 и чтение связей после подключения из PR #1353 — это нужно согласовать с их авторами. Новое от deffector: reads (sample({ source }) не создаёт ребро next от source) и dispose (clearNode сейчас ничего не сообщает). Связи отправляются на микрозадаче, потому что sample прикрепляет source уже после создания узла.
6. Бюджет шагов
У цикла ядра нет ограничения, поэтому синхронный цикл через события вешает вкладку. Реализация — limitSteps({ steps, fn }) из effector/inspect: лимит шагов на корневой launch, а при превышении — остановка и ошибка с самыми частыми узлами. Необязательное и спорное изменение: мейнтейнеры раньше называли обнаружение циклов нерешаемой задачей. Собственная аварийная остановка deffector работает и без него.
Проверка
| Ветка | База | Тесты |
|---|---|---|
deffector/1-safe-inspector | master | +2; без фикса оба падают |
deffector/2-message-seq | master | +3 |
deffector/3-all-scopes | master | +4 |
deffector/4-cause | deffector/2-message-seq | +3 |
deffector/5-links-dispose | master | +3 |
deffector/6-step-budget | master | +3 |
deffector/all | все вместе | 777 из 777 тестов проекта effector; 13 файлов, +790/−24 |
Сборка effector (node tools/build.js) проходит, и агент deffector прогнан на ней: 27 из 27 тестов vitest.upstream.config.ts, включая тест, который проверяет точные skip, caller и fork({ name }).
Чтобы прогнать upstream-тесты агента на такой сборке:
EFFECTOR_DIST=…/npm/effector npx vitest run -c vitest.upstream.config.tsПлан
- Discussion в effector/effector со ссылкой на прототип deffector.
- PR 1 сразу.
- PR 2–4 по одному, каждый с тестами и замером на бенчмарке из 200 тысяч событий; без инспектора разницы быть не должно.
- PR 5 совместно с авторами #1334 и #1353.
- Цель — минорная версия 23.x, затем перенос в
release/v24. - deffector определяет возможности по наличию полей в сообщениях, а не по номеру версии effector, поэтому работает и до, и после.