Контекст
Последние исправления реентрантности (#196, #207, #209, #210) довели текущую реализацию до корректного поведения при hostile host:
- host-write может синхронно запустить successor;
- successor подхватывает уже видимое значение и скорость;
- старый owner после потери владения не публикует stale write-back;
- settle-поколение несёт нулевую производную;
- бросок host сохраняет последнее успешно rendered-поколение;
- lifecycle не зависит от поздней подмены
Promise/queueMicrotask.
Поведение уже защищено сильными differential и hostile-host тестами. Issue не про исправление известного пользовательского бага и не про смену публичной семантики. Это следующий архитектурный шаг: сделать существующий инвариант поколения явной моделью данных.
Проблема
Сейчас понятие «поколение поверхности» распределено между связанными полями и фазами выполнения:
- live / applying value;
- rendered value;
- live / rendered velocity;
_tweenK / _renderedTweenK;
_writing;
- owner / reservation / transition;
- residual snapshot;
- settle и successor paths.
Корректность достигнута, но часть закона выражена согласованностью нескольких mutable-полей. Новая реализация GroupOwner или изменение capture/write/settle может воспроизвести класс ошибок #196, если не повторит неявный протокол полностью.
JSDoc GroupOwner уже фиксирует это как временное состояние и указывает направление: перенести инвариант в generation/lease на GroupRecord.
Цель
Сделать SurfaceGeneration и OwnerLease явными внутренними сущностями и единым источником истины для capture, host-write, rendered snapshot, settle и supersede.
Пример концептуальной формы — не обязательный API:
interface SurfaceGeneration {
readonly id: GenerationId
readonly numeric: ReadonlyMap<string, ChannelSnapshot>
readonly css?: CssSnapshot
readonly phase: 'applying' | 'rendered' | 'settled'
}
interface OwnerLease {
readonly generation: GenerationId
readonly owner: GroupOwner
}
Фактическая структура должна быть выбрана по hot-path стоимости и size-бюджету. Не следует механически добавлять объекты/Map/строковые discriminant в runtime.
Инварианты
- Одна наблюдаемая поверхность — одно поколение. Значения и скорости всех каналов transform-группы публикуются атомарно.
- Applying уже наблюдаемо. Во время host setter реентрантный capture видит применяемое поколение, хотя rendered commit фиксируется только после успешного возврата host.
- Rendered означает успешную host-запись. При throw последнее rendered-поколение не меняется.
- Settled несёт скорость покоя. Все числовые velocity и CSS derivative равны нулю.
- Lease определяет право публикации. Owner после supersede не может выполнить write-back, release или terminal publication.
- Successor наследует единый snapshot. Нельзя смешать value текущего поколения с velocity предыдущего.
- rAF и WAAPI следуют одному контракту. Различаются механизмы host transaction, но не семантика поколения.
- Residual state не является вторым источником истины. Это производное представление подтверждённого поколения.
Требования к дизайну
- сохранить публичный API и наблюдаемую семантику;
- не ухудшить continuity при interruption/supersede;
- не добавить аллокации на кадр;
- не добавить Promise/deferred на unit;
- не ухудшить Nano/Full size gates;
- не переносить host-specific детали в общий semantic layer;
- удалить
_writing и параллельные live/rendered поля только если новая модель полностью заменяет их; не создавать третий параллельный канал;
- один SSOT для generation identity и lease validation;
- failure atomicity должна следовать структуре, а не порядку ручных присваиваний.
Предлагаемый порядок TDD
RED — модельные тесты
Сначала зафиксировать контракт через независимую модель поколений:
- applying → reentrant capture;
- applying → host throw;
- applying → successor supersede;
- settle → reentrant successor;
- stale owner → write-back/release no-op;
- numeric и CSS поколения не смешиваются;
- rAF/WAAPI trace-equivalence для одинаковой поверхности.
GREEN — минимальный перенос
- Ввести generation/lease primitive без изменения поведения.
- Перевести capture на чтение поколения.
- Перевести host-write commit/rollback.
- Перевести write-back и settle.
- Перевести WAAPI reservation/release.
- Удалить заменённые параллельные поля и временные ветви.
REFACTOR — закрытие долга
- удалить временный
_writing-протокол;
- сократить JSDoc с историей багов до устойчивых инвариантов;
- проверить, что
GroupRecord не хранит две независимо изменяемые версии одного состояния;
- оставить mutation/property tests, запрещающие возврат к split-generation модели.
Приёмка
- все существующие hostile-host и continuity тесты проходят без ослабления;
- browser differential проходит на Chromium, Firefox и WebKit;
- новый model/property corpus покрывает interleaving поколений и lease;
- нет новых frame-path аллокаций;
- bundle size не регрессирует сверх установленного бюджета;
- benchmark start/frame/supersede не показывает значимой регрессии;
_writing либо удалён, либо доказано, почему он остаётся частью единой generation-модели;
- в коде существует один явный ответ на вопросы:
- какое поколение сейчас применимо;
- какое последнее успешно rendered;
- кто имеет право его опубликовать;
- какое поколение должен получить successor.
Не входит
- изменение публичного API
animate;
- новые animation features;
- изменение spring/tween математики;
- общий переписанный scheduler;
- усложнение Nano ради внутренней эстетики.
Задача оправдана только если новая модель уменьшает число представимых некорректных состояний без ухудшения размера и горячего пути.
Контекст
Последние исправления реентрантности (
#196,#207,#209,#210) довели текущую реализацию до корректного поведения при hostile host:Promise/queueMicrotask.Поведение уже защищено сильными differential и hostile-host тестами. Issue не про исправление известного пользовательского бага и не про смену публичной семантики. Это следующий архитектурный шаг: сделать существующий инвариант поколения явной моделью данных.
Проблема
Сейчас понятие «поколение поверхности» распределено между связанными полями и фазами выполнения:
_tweenK/_renderedTweenK;_writing;Корректность достигнута, но часть закона выражена согласованностью нескольких mutable-полей. Новая реализация
GroupOwnerили изменение capture/write/settle может воспроизвести класс ошибок#196, если не повторит неявный протокол полностью.JSDoc
GroupOwnerуже фиксирует это как временное состояние и указывает направление: перенести инвариант в generation/lease наGroupRecord.Цель
Сделать
SurfaceGenerationиOwnerLeaseявными внутренними сущностями и единым источником истины для capture, host-write, rendered snapshot, settle и supersede.Пример концептуальной формы — не обязательный API:
Фактическая структура должна быть выбрана по hot-path стоимости и size-бюджету. Не следует механически добавлять объекты/Map/строковые discriminant в runtime.
Инварианты
Требования к дизайну
_writingи параллельные live/rendered поля только если новая модель полностью заменяет их; не создавать третий параллельный канал;Предлагаемый порядок TDD
RED — модельные тесты
Сначала зафиксировать контракт через независимую модель поколений:
GREEN — минимальный перенос
REFACTOR — закрытие долга
_writing-протокол;GroupRecordне хранит две независимо изменяемые версии одного состояния;Приёмка
_writingлибо удалён, либо доказано, почему он остаётся частью единой generation-модели;Не входит
animate;Задача оправдана только если новая модель уменьшает число представимых некорректных состояний без ухудшения размера и горячего пути.