Skip to content

Внутреннее устройство

Эта страница объясняет, что происходит под публичным API Virentia. Для обычной разработки приложений это знать не обязательно. Раздел полезен, если вы пишете адаптеры, разбираете порядок выполнения или решаете, где должен жить новый примитив.

Юниты — это ноды графа

Публичное API говорит про сторы, события, эффекты и реакции. Внутри каждый из этих примитивов владеет нодами графа или создает их. Нода — маленький исполняемый шаг графа. Ноды связаны через next, поэтому один юнит может запустить другой, не зная, кто именно к нему подключен.

A unit run moves through graph nodesEvery queued work item carries a payload and a scope, so the graph can update the right state instance.
runenqueuenextwritenotify

Когда внешняя граница запускает событие, Virentia не обходит весь граф рекурсивно. Вместо этого она кладет задачу в kernel queue. В задаче есть нода, payload, scope и контекст выполнения. После этого queue последовательно выполняет ноды.

Поэтому payload и scope идут вместе. Payload говорит следующей ноде, что произошло. Scope говорит сторам, где читать и писать значения.

Сторы — определения, scopes хранят значения

Стор — не само значение. Стор владеет стабильной identity и знает, как по ней читать или писать данные. Scope хранит фактические значения.

The model is shared, values are scopedA store definition has one identity. Each scope keeps its own value for that identity.
ownslookuplookupread/writeread/write

Когда код читает query.value, стор берет scope из текущего контекста выполнения и ищет в этом scope собственный id. Если значения еще нет, стор возвращает начальное значение.

Когда код пишет query.value = "docs", стор запускает свою ноду в текущем scope. Нода записывает новое значение в scope.values и уведомляет подписчиков, которые смотрят на тот же scope.

Благодаря этому код модели можно переиспользовать. Модель можно импортировать один раз, а каждый экземпляр приложения, запрос, тест или кешированный экран получит собственную карту значений.

Реакции — связи с поведением

Явная реакция прикрепляет свою ноду к исходному юниту.

Например, если реакция слушает queryChanged, Virentia добавляет ноду реакции в queryChanged.node.next. Когда событие запускается, kernel доходит до этой ноды с тем же payload и scope. Тело реакции может писать сторы, вызывать эффекты или запускать другую логику модели.

Автоматические реакции — основной режим для большинства правил модели. При создании они запускаются один раз и запоминают сторы, которые были прочитаны во время run. Эти ноды сторов становятся зависимостями. Когда одна из зависимостей меняется, реакция запускается снова и обновляет список зависимостей.

Явные реакции остаются альтернативой для мест, где важна конкретная причина запуска: событие, эффект или юнит жизненного цикла. В таком коде on делает источник и payload частью правила.

Эффекты — это цепочки нод

Эффект — не просто async-функция. Это небольшой граф вокруг асинхронной операции.

Effects are node chains with lifecycle unitsThe call starts lifecycle state, awaits the handler, then settles into success or failure units.
enqueuenextresultstatus donestatus fail

Стартовая нода увеличивает $inFlight, обновляет $pending и вызывает started. Нода выполнения делает await handler. Завершающая нода уменьшает $inFlight, обновляет $pending и вызывает юниты успешного или неуспешного завершения.

Этот жизненный цикл доступен как обычные юниты: done, doneData, fail, failData, settled, $pending и $inFlight. Код модели может реагировать на них так же, как на события.

Отмена привязана к каждому выполняющемуся вызову. Handler получает AbortSignal, а dispose у владельца может отменить вызовы эффекта, созданные внутри него.

Kernel Queue

Kernel queue задает контролируемый порядок выполнения графа. Нода может вернуть значение, остановить текущую ветку, завершить ее ошибкой или поставить следующие ноды в очередь.

Каждая задача в очереди несет:

  • ноду, которую нужно запустить;
  • payload, с которым выполняется эта ветка;
  • текущее значение, полученное от предыдущей ноды;
  • scope;
  • контекст выполнения;
  • метаданные для интеграций.

Scope всегда часть задачи в очереди. Это важный момент: если юнит стартовал в scope, следующие ноды получают тот же scope, если низкоуровневая интеграция намеренно не меняет его.

Границы и контекст scope

allSettled(unit, { scope }) — самая явная граница, потому что scope передается напрямую. Он запускает работу графа в заданном scope и ждет завершения асинхронной работы.

scoped(scope, fn) — короткая рамка выполнения. Он кладет scope в текущий контекст выполнения, чтобы обычный код мог читать и писать сторы. Когда callback-функция завершается, предыдущий контекст scope восстанавливается.

Если callback возвращает promise, scoped(scope, fn) сохраняет этот scope для promise-цепочки до ее завершения. Это удобно для асинхронной работы, которой владеет приложение, но scoped не стоит воспринимать как универсальный async context для любого параллельного сценария.

scoped(scope).wrap(fn) нужен для интеграций. Он один раз захватывает scope и снова открывает его, когда другая библиотека позже вызывает вашу callback-функцию.

Владельцы и очистка

Владельцы нужны, потому что модели, созданные во время выполнения, должны уметь отвязывать свою работу. Реакции, подписки и функции очистки, зарегистрированные внутри owner, привязаны к нему.

Когда вызывается dispose, Virentia запускает функции очистки и отвязывает связи графа, созданные внутри владельца. Так динамические модели не оставляют реакции после закрытия модального окна, вкладки или кешированного экрана.

Практическая польза

Большая часть прикладного кода не должна думать о нодах. Она должна говорить на языке сторов, событий, эффектов, реакций, скоупов и владельцев.

Модель нод важна, когда вы строите bindings для фреймворков, слой совместимости, helpers для persistence, тестовые helpers, devtools или новый примитив. На этом уровне вопросы всегда одни: какая нода запускается, какой payload идет дальше, какой scope владеет значениями и кто потом очищает связи.