Skip to content

@virentia/core API

Используйте @virentia/core, чтобы строить модели состояния.

scope

Создает изолированный контейнер значений.

Используйте scope, когда одна и та же модель должна работать без общего состояния: в браузерном приложении, запросе, тесте, смонтированном виджете или кешированном экране.

ts
import { scope } from "@virentia/core";

const appScope = scope();

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

Задавайте значения сторов, обработчики эффектов и зависимости при создании:

ts
const appScope = scope({
  values: [[count, 10]],
  handlers: [[loadFx, async (id) => localData[id]]],
  deps: [[api, new RealApiClient()]],
});

values — сериализуемое состояние; handlers переопределяют реализации эффектов; deps предоставляют per-scope зависимости (см. dependency). handlers и deps никогда не сериализуются.

scoped

Запускает функцию в scope. Если функция возвращает promise, тот же scope сохраняется для этой promise-цепочки до ее завершения.

ts
import { store, scoped } from "@virentia/core";

const count = store(0);

scoped(appScope, () => {
  count.value = 1;
});

Используйте scoped, чтобы читать и писать сторы напрямую.

ts
await scoped(appScope, async () => {
  const response = await fetch("/api/count");
  count.value = (await response.json()).count;
});

scoped также может создать runner для повторного запуска и callback-функций.

ts
const inAppScope = scoped(appScope);

await inAppScope(() => loadFx());

const onMessage = inAppScope.wrap((message: string) => {
  messages.items = [...messages.items, message];
});

Если код уже выполняется внутри scope, его можно не передавать.

ts
scoped(() => {
  count.value += 1;
});

dependency

Объявляет per-scope инъекцию — API-клиент, часы, логгер. Это обвязка, а не состояние: каждый scope предоставляет свой экземпляр, и в отличие от стора она никогда не сериализуется и не гидрируется.

ts
import { dependency, effect, provideDependency, scope } from "@virentia/core";

const api = dependency<ApiClient>("api");

const loadFx = effect(async (id: string) => api.value.get(id));

// Предоставьте при создании или императивно.
const appScope = scope({ deps: [[api, new RealApiClient()]] });
provideDependency(appScope, api, new RealApiClient());

Читайте dep.value под активным scope (обработчик эффекта, тело реакции, scoped). Чтение зависимости — не реактивная зависимость. Чтение той, что активный scope не предоставил, бросает понятную ошибку. provideDependency(scope, dep, value) задаёт её императивно.

Полное руководство — Зависимости.

store

Создает записываемый стор, значение которого хранится в scope.

Используйте стор для состояния, которым владеет модель. Одно определение стора может иметь разные значения в разных scopes.

ts
const count = store(0);
const profile = store({ name: "Ada", age: 36 });

scoped(appScope, () => {
  count.value += 1;
  profile.value = { ...profile.value, age: 37 };
});

Производные сторы:

ts
const doubled = count.map((value) => value * 2);
const positive = count.filter((value) => value > 0);
const label = count.filterMap((value) => (value > 0 ? `#${value}` : "skip"), "skip");

map, filter и filterMap создают ленивые read-only сторы. Без подписки они пересчитываются только при чтении. Если на них подписана реакция или UI, они пересчитываются при изменении зависимостей.

Подписка на обновления в scope:

ts
const unsubscribe = count.subscribe((value, scope) => {
  console.log(value, scope);
});

unsubscribe();

computed

Создает read-only стор с ленивым вычислением.

ts
const visibleUsers = computed(() => {
  const text = query.value.toLowerCase();

  return users.items.filter((user) => user.name.toLowerCase().includes(text));
});

computed запоминает результат отдельно в каждом scope. Зависимости определяются автоматически по сторам, прочитанным внутри функции. Без активных подписок вычисление не запускается после изменения зависимостей, пока значение не прочитают.

lazyModel

Создает ленивую оболочку модели.

Используйте lazyModel, когда модель вынесена в отдельный модуль и должна импортироваться только при запуске или вызове одного из ее юнитов.

ts
const chat = lazyModel(() =>
  import("./chat.model").then(({ createChatModel }) => createChatModel()),
);

await scoped(appScope, () => chat.opened("chat:1"));

Реакции могут подписываться на ленивые события и lifecycle-юниты эффектов до загрузки модуля. Чтение сторов остается синхронным, поэтому читайте сторы ленивой модели после того, как модель уже загрузилась.

event

Создает вызываемое событие.

Используйте событие, когда модель должна узнать, что что-то произошло. События несут payload и запускают связанные реакции.

ts
const submitted = event<{ text: string }>();

Обрабатывайте событие через реакции:

ts
reaction({
  on: submitted,
  run({ text }) {
    query.value = text;
  },
});

Производные события:

ts
const textOnly = submitted.map(({ text }) => text);
const nonEmpty = textOnly.filter((text) => text.length > 0);
const normalized = nonEmpty.filterMap((text) => text.trim() || undefined);

effect

Создает вызываемый юнит для работы с внешним миром.

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

ts
const loadUserFx = effect(async (id: string, { signal }) => {
  const response = await fetch(`/api/users/${id}`, { signal });
  return (await response.json()) as { id: string; name: string };
});

Юниты эффекта:

ts
loadUserFx.started;
loadUserFx.done;
loadUserFx.failed;
loadUserFx.fail;
loadUserFx.doneData;
loadUserFx.failData;
loadUserFx.finally;
loadUserFx.settled;
loadUserFx.abort;
loadUserFx.aborted;

Сторы эффекта:

ts
loadUserFx.pending;
loadUserFx.inFlight;

Вызов внутри scope:

ts
const user = await scoped(appScope, () => loadUserFx("user:1"));

Отмена выполняющихся вызовов:

ts
await scoped(appScope, () => loadUserFx.abort(new Error("cancelled")));

Отмена завершает активные вызовы на уровне рантайма Virentia, поэтому handler не обязан читать signal или вручную делать reject. Эффекты, запущенные активным эффектом, автоматически наследуют отмену родителя и отменяются с той же причиной.

attach

Создает новый эффект, который перед запуском читает source-сторы и собирает params.

ts
const authorizedFx = attach({
  source: token,
  effect: requestFx,
  mapParams: (id: number, token: string) => ({ id, token }),
});

source может быть одним стором, массивом сторов или объектом сторов. Если effect — существующий эффект, attach переиспользует его обработчик, а жизненный цикл остается у нового эффекта.

reaction

Создает правило модели.

По умолчанию начинайте с автовычисления зависимостей: передайте функцию, прочитайте внутри нужные сторы, и Virentia сама поймет, от каких значений зависит реакция. Такая реакция пересобирает зависимости при каждом запуске.

Автоматическая реакция:

ts
reaction(() => {
  fullName.value = `${firstName.value} ${lastName.value}`;
});

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

Явный on:

ts
reaction({
  on: submitted,
  run(payload) {
    console.log(payload);
  },
});

Несколько источников:

ts
reaction({
  on: [firstChanged, secondChanged],
  run(payload) {
    console.log(payload);
  },
});

Опции scope и инспектора:

ts
reaction({
  on: ticked,
  scope: appScope, // Scope | readonly Scope[] — выполнять только в этих scopes
  name: "tick", // подпись в инспекторе
  key: true, // пометить как keyed-узел в инспекторе
  run() {},
});

Остановить реакцию:

ts
const subscription = reaction({
  on: submitted,
  run() {},
});

subscription.stop();

owner

Создает границу жизненного цикла.

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

Колбэк получает (dispose, owner): используйте dispose(), чтобы свернуть границу изнутри, и owner, чтобы передать границу во вложенные фабрики.

ts
const model = owner(() => {
  const incremented = event<void>();
  const count = store(0);

  reaction({
    on: incremented,
    run() {
      count.value += 1;
    },
  });

  return { count, incremented };
});

model.dispose();

Корневой объект модели также получает [Symbol.dispose], поэтому в средах с поддержкой Explicit Resource Management можно использовать using.

ts
{
  using model = owner(() => {
    return { count: store(0) };
  });
}

onCleanup, getOwner, withOwner

Используйте cleanup-утилиты, когда вспомогательная функция создает таймеры, подписки, browser listeners или другой ресурс, который нужно отвязать вместе с моделью.

ts
owner((dispose) => {
  const timer = setInterval(() => {}, 1000);

  onCleanup(() => {
    clearInterval(timer);
  });

  return { dispose };
});

Подключить очистку к уже известному владельцу. withOwner(owner, fn) делает переданного владельца текущим только на время выполнения fn:

ts
const model = owner((dispose, currentOwner) => {
  return { dispose, owner: currentOwner };
});

withOwner(model.owner, () => {
  onCleanup(() => {
    console.log("cleanup");
  });
});

Прочитать текущего владельца внутри вспомогательной функции:

ts
const current = getOwner();

node и run

Низкоуровневый API графа для интеграций, экспортируется из @virentia/core/internal (не из основного входа — см. Низкоуровневое ядро).

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

ts
import { node, run } from "@virentia/core/internal";

const reader = node((ctx) => {
  console.log(ctx.value);
});

await run({
  unit: reader,
  payload: "hello",
  scope: appScope,
});

Внутри ноды ctx.stop() останавливает текущую ветку, ctx.fail(error) останавливает ее как ошибочную, а ctx.launch(unit, value) добавляет другую ноду или юнит в очередь в том же scope и контексте выполнения.

@virentia/core/internal также экспортит примитивы трекинга (trackNode, collectNodes, isTracking), доступ к активному scope (getActiveScope, requireActiveScope, setActiveScope) и жизненный цикл транзакций (writeTransactionStore, readTransactionStore, …) для написания собственных сторов. См. Низкоуровневое ядро.

context и withContexts

Передают метаданные через одну цепочку выполнения kernel. Экспортируются из @virentia/core/internal.

Используйте contexts для данных, которые относятся к одному запуску: request id, tracing, служебные флаги адаптеров. Для состояния приложения используйте сторы.

ts
import { context, withContexts } from "@virentia/core/internal";

const requestId = context<string>();

withContexts([requestId.setup("request-1")], () => {
  console.log(requestId.get());
});

Внутри ноды:

ts
const reader = node((ctx) => {
  console.log(ctx.getContext(requestId));
});