Skip to content

feat(compositor): добавить общий follow с native settle - #458

Closed
lemone112 wants to merge 16 commits into
mainfrom
codex/motion-generic-follow-20261001
Closed

lemone112 wants to merge 16 commits into
mainfrom
codex/motion-generic-follow-20261001

Conversation

@lemone112

@lemone112 lemone112 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Добавляет опциональный @labpics/motion/compositor/follow: координаты жеста передаются в native spring, а текущую анимацию можно снова захватить. Snap points, страницы и фокус принадлежат потребителю. Опубликованные recipes и component exports сохранены.

Исправляет владение ресурсами MotionValue и Svelte. Повторная доставка одной заявки на кадр больше не исполняет её второй раз; новая анимация продолжает движение после stop() → setTarget() из getter/listener. Освобождённые подписки перестают удерживать callbacks, а ошибки listener сохраняют порядок уведомления соседей. Повторная Svelte-подписка с тем же run теперь возвращает один текущий off object — это наблюдаемое изменение identity.

HEAD c59e47ae3a7083a2469b878b89ca807e81bdb6ba: все 10 проверок GitHub CI прошли, включая package/consumer contract и Chromium/Firefox/WebKit. Исправленный carousel-тест различает корректный pickup и намеренно подставленную старую позицию. Для последнего изменения дополнительно прошли 174 затронутых теста, ESM/CJS consumer-сценарии и две независимые проверки жизненного цикла и совместимости host. Размеры в sandbox: full-core 2319/2330 B gzip, stagger 6597/6600 B gzip; лимиты не изменены.

Содержательное CodeRabbit review текущего HEAD запрошено. Полные RESOURCE/M-07, JOURNEY и PROFILE остаются открытыми; PR остаётся draft. Merge/release/deploy не выполнялись.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@lemone112

Copy link
Copy Markdown
Contributor Author

@coderabbitai Нужна предметная проверка одной оси в текущем HEAD 33f496a0b8350b1902b5c7651b2886ecf95102ed: владение и освобождение ресурсов нового ./compositor/follow (src/compositor/follow.ts, общий lifecycle в src/compositor/core.ts, тесты test/compositor-follow.test.ts и test/resource-actual-package-retention.test.ts). Это обычный scoped chat; платный либо полный review не запрашивается.

Проверьте достижимые истории beginFollow → follow → settle → destroy и settle → pickup → destroy, повторный вход/ошибки callbacks и сохранность владельца при отмене. Примитив должен быть общим: snap/page/focus policy остаётся в recipes, существующие публичные component exports сохранены. Нужны конкретные оставшиеся ошибки на этой оси либо явный вывод, что их не найдено, с границами статического анализа.

Новый CI завершился SUCCESS, 10/10 jobs. Installed ESM/CJS retention GREEN; отдельный мутант без super.destroy() ловится cancellation assertion до GC. Это не утверждение о native/GPU bytes, физическом mobile profile или полном RESOURCE-01. PR остаётся draft, merge/release не запрашиваются.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git rev-parse HEAD
git status --short
git diff --stat 957ceaf26501525eff476cdc1d149d81fd5ebcd7...33f496a0b8350b1902b5c7651b2886ecf95102ed
for f in src/compositor/follow.ts src/compositor/core.ts test/compositor-follow.test.ts test/resource-actual-package-retention.test.ts; do
  ast-grep outline "$f" --items all
done
cat -n package.json

Length of output: 28057


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/compositor/follow.ts
sed -n '340,1015p' src/compositor/core.ts | nl -ba -v340
cat -n test/compositor-follow.test.ts
cat -n test/resource-actual-package-retention.test.ts
git diff 957ceaf26501525eff476cdc1d149d81fd5ebcd7...HEAD -- src/compositor/core.ts package.json src/compositor/follow/index.ts
ast-grep outline src/internal/velocity-tracker.ts --items all
ast-grep outline browser/fixtures/compositor-follow-recipes.mjs --items all
rg -n -C3 'beginFollow|pickup|ownership|destroy|follow' docs/compositor.md

Length of output: 79983


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline src/motion-value.ts --items all
cat -n src/internal/velocity-tracker.ts
cat -n browser/fixtures/compositor-follow-recipes.mjs
ast-grep outline test/compositor-follow-recipes.test.ts --items all
cat -n test/compositor-follow-recipes.test.ts
cat -n test/compositor-follow-package.test.ts
rg -n -C3 'throw|reentra|cleanup|destroy|scheduler' test --iglob '*compositor*' --iglob '*lifecycle*'
rg -n -C3 'compositorFollow|beginFollow|AbortController|dragstart|ownerDocument' docs/recipes.md
git diff --name-only 957ceaf26501525eff476cdc1d149d81fd5ebcd7...HEAD -- src
fd -i 'compositor|motion-value|frame-loop' test src

Length of output: 119948


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '120,510p' src/motion-value.ts | nl -ba -v120
ast-grep outline src/internal/sliding-window.ts --items all
cat -n src/internal/sliding-window.ts
sed -n '415,570p' docs/recipes.md | nl -ba -v415
ast-grep outline test/compositor-host-owner.test.ts --items all
sed -n '572,638p' test/compositor-host-owner.test.ts | nl -ba -v572
sed -n '667,733p' test/compositor-host-owner.test.ts | nl -ba -v667
sed -n '773,849p' test/compositor-host-owner.test.ts | nl -ba -v773
ast-grep outline src/compositor/index.ts --items all
cat -n src/compositor/index.ts
cat -n src/compositor/stagger/index.ts

Length of output: 39258


@lemone112 Проверил фактический HEAD 33f496a0b8350b1902b5c7651b2886ecf95102ed. Рабочее дерево чистое. Проверка была статической и ограниченной указанной осью.

Оставшихся ошибок владения и освобождения ресурсов в проверенных историях не нашёл. Ниже — основания и границы этого вывода.

Владение и освобождение

История Результат статической проверки
beginFollow → follow → settle → destroy settle публикует native owner и отзывает input-сессию. destroy отсоединяет host-owner и вызывает его отмену.
settle → pickup → destroy beginFollow получает native snapshot. После успешной underlying-записи он отсоединяет donor. Последующий destroy не отменяет этот donor повторно.
Live fallback → pickup → settle Pickup останавливает прежний MotionValue до пользовательского writer. После перехода он уничтожает прежний MotionValue. Поздние кадры не пишут значение и не продолжают цикл.

Ключевые участки:

  • src/compositor/follow.ts, Line 31–70: snapshot, остановка live donor, публикация input-owner и обработка ошибки.
  • src/compositor/follow.ts, Line 129–136: отмена donor требует успешной записи и сохранения identity input-owner.
  • src/compositor/core.ts, Line 907–920: native successor публикуется до отмены donor.
  • src/compositor/core.ts, Line 953–970: stale live-кандидат уничтожается; принятый live-owner остаётся доступным контроллеру.

Повторный вход и ошибки callbacks

Проверил следующие переходы:

  • Если format или apply бросает без нового намерения, pickup восстанавливает прежнее логическое состояние. Native donor остаётся у контроллера.
  • Если callback выполняет вложенный follow, успешная вложенная запись может снять donor. Ошибка внешнего callback не восстанавливает прежнего владельца.
  • Если callback выполняет settle, retarget или handoffToLive, внешняя операция не снимает successor.
  • Если callback выполняет stop или destroy, внешняя операция не восстанавливает input-сессию.
  • Если animate() не возвращает successor из-за ошибки, код не отсоединяет donor.
  • Если scheduler бросает после live commit, контроллер сохраняет live-owner. Код не пытается восстановить уже отменённый donor.

_epoch защищает continuation. Identity _following отдельно защищает право отмены donor. Эти проверки нужны обе: вложенный follow сохраняет input-owner, но меняет _epoch.

Соответствующие случаи представлены в test/compositor-follow.test.ts, Line 205–367, и выбранных тестах общего host lifecycle.

Terminal cleanup и retention

InputSpring.destroy() очищает _following и вызывает super.destroy() — src/compositor/follow.ts, Line 124–126.

Общий destroy() сначала снимает _mv и обрывает ссылки на target, writer, formatter, scheduler, timer factory, clock и artifact. Затем он выполняет host cleanup и уничтожает сохранённый MotionValue — src/compositor/core.ts, Line 769–778. Повторный destroy() не создаёт ресурсов.

Installed-package retention test действительно содержит:

  • ESM и CJS;
  • обе запрошенные истории;
  • сильно достижимые controllers;
  • WeakRef-проверки target и effect;
  • положительные live controls;
  • cancellation assertions до GC.

Это видно в test/resource-actual-package-retention.test.ts, Line 189–268. Результат GREEN и результат мутанта принимаю как предоставленные вами данные. В этой проверке я не запускал тесты и не перепроверял CI.

Граница примитива и публичного API

Snap/page/focus policy остаётся в docs/recipes.md. Примитив получает только значения, timestamps и конечную цель.

Recipes снимают listeners через AbortSignal, освобождают pointer capture и вызывают motion.destroy(). Существующие component entrypoints не изменены. В export map добавлен отдельный ./compositor/follow; старые compositor barrels не получили createCompositorFollow.

Ограничения вывода

Это не исчерпывающее доказательство всех комбинаций повторного входа.

Если host cancel() бросает, код гарантирует логическое отсоединение, но не физическое освобождение native effect. Исключение поглощается по существующему контракту.

RequestFrameFn не предоставляет отмену очереди. Поэтому cleanup делает выданные callbacks инертными, но не удаляет их физически из scheduler.

Вывод не подтверждает native/GPU bytes, mobile profile, полный RESOURCE-01/M-07 или production readiness. Он также не является полным review либо разрешением на merge/release.

Спасибо за уточнение границ проверки.


✏️ Learnings added
Learnt from: lemone112
URL: https://github.com/Labpics-Team/lab-motion/pull/458

Timestamp: 2026-10-01T08:10:30.231Z
Learning: В Labpics-Team/lab-motion createCompositorFollow из labpics/motion/compositor/follow является общим scalar input/native-settle примитивом. Snap-point, page и focus policy принадлежат приложению и recipes в docs/recipes.md, а не src/compositor/follow.ts. Installed-package retention control в test/resource-actual-package-retention.test.ts проверяет collectability управляемого JavaScript graph при сильно достижимом controller; этот результат не доказывает освобождение native/GPU bytes и не закрывает полный RESOURCE-01/M-07.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@lemone112

Copy link
Copy Markdown
Contributor Author

@coderabbitai Проверьте одну ось на HEAD c59e47ae3a7083a2469b878b89ca807e81bdb6ba: владение и освобождение callbacks в изменениях после ранее проверенного 33f496a0b8350b1902b5c7651b2886ecf95102ed. Область — src/motion-value.ts, src/compositor/core.ts, Svelte-адаптер и связанные regression/resource tests.

Нужно проверить повторную доставку одной requestFrame-заявки, отдельный reentrant successor, синхронный injected host, порядок listener/scheduler ошибок и отозванные подписки. Сопоставьте с публичным RequestFrameFn/asRequestFrame и контрактом Svelte. Нужны конкретные оставшиеся ошибки либо вывод об их отсутствии с границами проверки. Это обычный scoped chat; платный или полный review не запрашивается. Текущий CI прошёл 10/10, merge/release не запрашиваются.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git rev-parse HEAD
git status --short
git diff --stat 33f496a0b8350b1902b5c7651b2886ecf95102ed c59e47ae3a7083a2469b878b89ca807e81bdb6ba
git diff --name-only 33f496a0b8350b1902b5c7651b2886ecf95102ed c59e47ae3a7083a2469b878b89ca807e81bdb6ba
ast-grep outline src/motion-value.ts --items all
ast-grep outline src/compositor/core.ts --items all
fd -i 'svelte|request.frame|retention|resource|lifecycle' src test
rg -n -C3 'RequestFrameFn|asRequestFrame' src
cat package.json

Length of output: 50216


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ast-grep outline src/svelte/index.ts --items all
ast-grep outline src/frame/index.ts --items all
for f in test/motion-value-frame-delivery.test.ts test/motion-value-reentrant-start.test.ts test/motion-value-listener-isolation.test.ts test/svelte-unsubscribe-lifecycle.test.ts test/resource-actual-package-retention.test.ts; do ast-grep outline "$f" --items all; done
cat -n src/motion-value.ts
cat -n src/svelte/index.ts
cat -n src/frame/index.ts
git diff 33f496a0b8350b1902b5c7651b2886ecf95102ed HEAD -- src/compositor/core.ts CHANGELOG.md
cat -n test/motion-value-frame-delivery.test.ts
cat -n test/motion-value-reentrant-start.test.ts
cat -n test/motion-value-listener-isolation.test.ts
cat -n test/svelte-unsubscribe-lifecycle.test.ts
git diff 33f496a0b8350b1902b5c7651b2886ecf95102ed HEAD -- test/motion-value.test.ts test/compositor-host-owner.test.ts test/resource-actual-package-retention.test.ts

Length of output: 92480


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git diff 33f496a0b8350b1902b5c7651b2886ecf95102ed HEAD -- src/motion-value.ts src/svelte/index.ts
sed -n '560,810p' src/compositor/core.ts | nl -ba -v560
sed -n '925,995p' src/compositor/core.ts | nl -ba -v925
ast-grep outline src/internal/solver.ts --items all
rg -n -C4 'function solveSpring|mass|stiffness|damping' src/internal/solver.ts
fd -i 'unsubscribe|retention|svelte|frame' test
rg -n -C3 'WeakRef|unsubscribe|onChange|springStore|throw|subscribe' test/frame-retention-gc.test.ts test/fixtures/frame-retention-gc.probe.ts test/svelte.test.ts test/svelte-current-value.test.ts
cat -n test/resource-actual-package-retention.test.ts | sed -n '80,185p;300,355p'
rg -n -C3 'subscribe|Svelte|RequestFrameFn|asRequestFrame' docs/bindings.md

Length of output: 62680


🤖 web_search executed:

Svelte official store contract subscribe synchronously immediately unsubscribe stop called store custom stores same subscriber function duplicate subscription

Length of output: 1533


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git show 33f496a0b8350b1902b5c7651b2886ecf95102ed:src/svelte/index.ts | nl -ba | sed -n '91,145p'
rg -n -C3 'toBe\(.*[Oo]ff|toBe\(.*first|subscribe.*destroy|initial.*throw|throw.*initial|WeakRef|destroy' test/svelte-unsubscribe-lifecycle.test.ts test/svelte.test.ts test/resource-actual-package-retention.test.ts || true
sed -n '807,840p' src/compositor/core.ts | nl -ba -v807

Length of output: 17071


🤖 web_search executed:

site.svelte.dev/docs/svelte/stores "Store contract" "synchronously" "unsubscribe" custom store contract

Length of output: 773


@lemone112 Проверил фактический HEAD c59e47ae3a7083a2469b878b89ca807e81bdb6ba. Рабочее дерево чистое. Сравнил изменения с 33f496a0b8350b1902b5c7651b2886ecf95102ed.

Новых ошибок владения callbacks в этом diff не нашёл. Однако в Svelte остаются два прежних пути удержания callbacks. Поэтому общий вывод «на этой оси ошибок нет» был бы слишком широким.

Проверенные изменения

Сценарий Вывод статической проверки
Повторная доставка одной requestFrame-заявки called погашает заявку до _tick. Повтор не публикует значение и не создаёт следующий кадр.
Отдельный reentrant successor stop() → setTarget() создаёт новый _run. Старый _tick не подавляет callback нового _run и не перепланирует старый _run.
Синхронный injected host Callback сохраняет timestamp. Физика выполняется через async trampoline. Проверка _run после возврата host предотвращает создание fallback после отмены.
Listener и scheduler бросают _emit доставляет значение оставшимся соседям. Первая listener-ошибка остаётся первичной, включая throw undefined. Ошибка scheduler отзывает только принадлежащий ему _run.
Отозванная подписка off() удаляет регистрацию и обрывает захваты callback/registry. Повтор старого off() не удаляет новую регистрацию.
Ошибка callback после его повторной регистрации _emit использует исходный off. Ошибка старой регистрации не отзывает её successor.

Основания: src/motion-value.ts, Line 220–243, Line 370–405, Line 408–429 и Line 474–500.

Regression tests проверяют дубль заявки, successor из getter/listener, синхронный successor, вложенную доставку старой заявки и порядок ошибок. Это не только проверки конечного значения: тесты проверяют число pending callbacks и timers.

В src/compositor/core.ts, Line 770–781, destroy() снимает пользовательские capabilities до host cleanup. Затем он уничтожает сохранённый MotionValue. Замена _spring на DEFAULT_SPRING освобождает исходные параметры без чтения getters. Инертный handoff не возвращает пользовательские параметры в работу.

Публичные контракты

RequestFrameFn / asRequestFrame. Тип возвращает числовой handle, но не предоставляет операцию отмены. asRequestFrame создаёт once-подписку фазы update и возвращает 1 — src/frame/index.ts, Line 296–300.

Новая защита совместима с этим контрактом. Заявка следующего кадра имеет собственный called. При отмене queued callback становится инертным. Физическое удаление callback из внешней очереди эта защита не гарантирует.

Svelte. Немедленная доставка текущего значения и прекращение уведомлений после off() соответствуют публичному store contract.

Повторный subscribe(run) возвращает тот же текущий off. Это наблюдаемое изменение identity, а не независимая регистрация. Новая семантика описана в интерфейсе. Поведенческие тесты проверяют отмену через оба handles. Прямого assertion на identity в просмотренных тестах нет.

Оставшиеся ограничения Svelte

Оба случая существовали уже в проверенном baseline. Это не регрессии данного diff.

1. Ошибка первой доставки оставляет регистрацию без выданного off

src/svelte/index.ts, Line 124–127:

subscribers.set(run, off);
run(mv.value);
return off;

Если run бросает, subscribe не возвращает off, но Map продолжает удерживать callback. Callback может снова получить уведомление при изменении значения. destroy() освобождает регистрацию, но сам неуспешный subscribe её не откатывает.

Это конкретный оставшийся пробел в exception-safe владении. В отличие от Svelte-адаптера, MotionValue.onChange выполняет rollback первой доставки.

2. Подписка после destroy() снова удерживает callback

У subscribe нет terminal guard. История destroy() → subscribe(run) добавляет callback в Map и сразу вызывает run. При этом уничтоженный MotionValue больше не доставляет изменения.

Если store остаётся достижимым, регистрация сохраняется до off() или повторного destroy(). Это граница terminal cleanup адаптера. Она не возникает из обычного повторного вызова уже отозванного handle.

Отдельно: fail-fast broadcast Svelte намеренно сохранён. Ошибка одного subscriber снимает весь broadcast-listener с MotionValue. Соседи не получают этот и последующие updates. test/svelte-unsubscribe-lifecycle.test.ts, Line 57–69, явно закрепляет это прежнее поведение. Не выдаю его за новую регрессию.

Границы проверки

  • Проверка статическая. Тесты и код репозитория не запускал.
  • CI 10/10 принимаю как предоставленный вами результат.
  • Installed ESM/CJS retention tests покрывают terminal MotionValue, захваты scheduler/listener и spring metadata при достижимых владельцах.
  • Этот installed probe не проверяет Svelte и два указанных пути регистрации.
  • Вывод не доказывает освобождение native/GPU bytes или все комбинации reentry.
  • Полный review, merge и release не запускал.

You are interacting with an AI system.

@lemone112

Copy link
Copy Markdown
Contributor Author

Интегрировано через #471 в ee78df3fcc3f9ff44c8f3ed717b3ee3767f2f121. Исходный head этого PR c59e47ae3a7083a2469b878b89ca807e81bdb6ba входит в историю проверенного кандидата fc2a579176078cc68b1ca63ea31de825bfaabe54; его дерево 38dbe02a6a098e28bf6737667238ee319e1f20a5 побайтно совпало с деревом поставленного коммита.

Обязательный CI кандидата прошёл, CodeRabbit одобрил этот head 5 октября 2026 в 15:23 UTC. Закрываю поглощённый PR с сохранением ветки. Открытые измерительные и продуктовые условия остаются в действующем плане r13.

@lemone112 lemone112 closed this Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant