Низкоуровневое ядро
Большинство прикладного кода должно использовать сторы, события, эффекты, реакции и owners — основной вход @virentia/core.
Низкоуровневые строительные блоки ядра лежат в отдельном подпути @virentia/core/internal: прямые узлы графа, контексты выполнения, трекинг зависимостей, доступ к scope и жизненный цикл транзакций. Он нужен, чтобы писать интеграцию с сырыми узлами или собственный юнит/стор — @virentia/mutable целиком построен на нём.
Продвинутая поверхность
@virentia/core/internal — намеренно отдельный вход: прикладному коду он не нужен. Он реэкспортит те же singleton-модули, что и основной вход (бандлер выносит их в общий чанк), поэтому пакет, построенный на нём, разделяет scope, транзакции и граф ядра, а не получает свою копию. Считайте его продвинутым и менее стабильным, чем основной API. Из основного входа публичны только типы ядра (например Node, который есть на каждом юните как .node).
node и run
import { node, run } from "@virentia/core/internal";
import { scope } from "@virentia/core";
const appScope = scope();
const logNode = node((ctx) => {
console.log(ctx.value);
});
await run({
unit: logNode,
payload: "hello",
scope: appScope,
});Ноды могут передавать значения нижележащим нодам.
const second = node((ctx) => {
console.log(ctx.value); // "next"
});
const first = node({
run: () => "next",
next: [second],
});
await run({ unit: first, scope: appScope });Нода может явно поставить в очередь другую ветку. ctx.launch сохраняет текущий scope, contexts, metadata и batch key, но даёт ноде выбрать, какой юнит получит следующее значение.
const gate = node((ctx) => {
ctx.stop();
ctx.launch(second, "next");
});ctx.stop() останавливает текущую ветку; ctx.fail(error) останавливает её как ошибочную.
Трекинг зависимостей
Собственный стор делает свои чтения реактивными, репортя их окружающему computed или реакции. Вызовите trackNode(node) из чтения: внутри окна трекинга это регистрирует node как зависимость, поэтому последующий запуск этой ноды перезапустит читателя.
import { node, run, trackNode } from "@virentia/core/internal";
const storeNode = node({ run: (ctx) => ctx.value });
function read(scope: Scope): Value {
trackNode(storeNode); // это чтение теперь зависит от `storeNode`
return currentValue(scope);
}
function commit(scope: Scope, next: Value): void {
void run({ unit: storeNode, payload: next, scope }); // перезапустит читателей `storeNode`
}isTracking() возвращает, зарегистрируется ли чтение прямо сейчас — используйте, чтобы не строить мелкогранулярные ключи зависимостей на чтениях, которые никто не трекает (обычная мутация), сохраняя путь записи дешёвым. collectNodes(fn) выполняет fn и возвращает { result, nodes } — набор нод, которые он затрекал; тем же механизмом computed собирает свои зависимости.
Про трекинг по keypath — подписку читателя только на прочитанные им части значения — смотрите, как @virentia/mutable мапит keypath'ы на лениво созданные ноды.
Активный scope
Значения сторов живут в scope, поэтому собственный стор резолвит активный на каждом чтении или записи.
import { getActiveScope, requireActiveScope, setActiveScope } from "@virentia/core/internal";
const scope = requireActiveScope(() => "read a counter store");requireActiveScope(describe) возвращает активный scope или бросает внятную ошибку «Scope is required», используя describe() и путь юнитов, чтобы сказать, что именно требовало scope. getActiveScope() возвращает его или null. setActiveScope(scope) меняет ambient-scope и возвращает предыдущий — низкоуровневый примитив под scoped.
Интеграция с транзакциями
Записи в сторы батчатся и применяются на границе транзакции (см. Транзакции). Собственный стор подключается к этой границе через writeTransactionStore:
import { writeTransactionStore } from "@virentia/core/internal";
import type { StoreCommitResult, StoreTransactionTarget } from "@virentia/core/internal";
const id = Symbol("my-store");
const target: StoreTransactionTarget = {
id, // дедупит повторные записи в этот стор в одной транзакции
scope,
commit(): StoreCommitResult {
const changed = applyStagedValueToScope();
return {
changed,
notify: () => notifySubscribersAndFireNodes(),
};
},
};
writeTransactionStore(target, stagedValue);На границе ядро вызывает commit() каждого target ровно один раз (повторные записи с тем же id + scope дедупятся), применяет изменения, затем прогоняет все notify — так подписчики видят финальное закоммиченное значение. readTransactionStore(target) читает застейдженное значение обратно внутри той же транзакции. Транзакции никогда не откатываются.
Именно так @virentia/mutable откладывает целую глубокую правку до одного атомарного коммита — полный рабочий пример есть в его исходниках.
Контексты ядра
Контексты ядра прокидывают метаданные выполнения через цепочку run.
import { context, node, run } from "@virentia/core/internal";
const requestId = context<string>();
const reader = node((ctx) => {
console.log(ctx.getContext(requestId));
});
await run({
unit: reader,
scope: appScope,
contexts: [requestId.setup("request-42")],
});Используйте сторы для прикладного состояния. Используйте контексты ядра для метаданных, принадлежащих одному выполнению. withContexts([...], fn) задаёт контексты вокруг синхронного вызова вне ноды.
Механика
Пользовательская модель транзакций описана в Транзакциях. Механика исполнения и обоснование дизайна — в Deep Knowledge.