Skip to content

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

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

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

Owner - низкоуровневый примитив

Для доменной сущности - у которой есть данные, id и место в списках - берите модели: model и staticModel строятся на той же семантике времени жизни и добавляют коллекции, разбор и сериализацию JSON, связи, запросы и onCleanup на экземплярах. Голый owner оставьте служебным подсистемам, которые сущностями не являются: сокетам, плеерам, кешам, фоновым процессам. Сравнение - на странице Виды моделей.

Динамическая работа под владельцем

Владелец дает динамической работе один жизненный цикл. Все, что создано внутри owner, можно удалить вместе.

ts
import { event, onCleanup, owner, reaction, store } from "@virentia/core";

export function createDraftModel() {
  return owner(() => {
    const changed = event<string>();
    const text = store("");

    reaction({
      on: changed,
      run(value) {
        text.value = value;
      },
    });

    return { changed, text };
  });
}

owner добавляет dispose() на корневой объект модели. Когда черновик закрывается, вызовите dispose. Реакции, созданные внутри владельца, будут отвязаны вместе с ним.

ts
const draft = createDraftModel();

draft.dispose();

Если среда выполнения поддерживает using и Symbol.dispose, можно доверить очистку блоку кода:

ts
{
  using draft = createDraftModel();

  // работа с draft
}

WARNING

Symbol.dispose и using еще не одинаково доступны во всех JavaScript runtimes, включая Safari без транспиляции. Если ваш runtime, bundler или транспайлер их не поддерживает, используйте обычный model.dispose().

Вложенные владельцы уходят вместе с родителем

Владелец, созданный внутри тела другого владельца, — его ребёнок. Роспуск родителя распускает всех детей, начиная с самого вложенного: так cleanup ребёнка ещё может опираться на то, что держит родитель.

ts
const screen = owner(() => {
  const list = owner(() => {
    reaction({ on: filtersChanged, run: refetch });

    return {};
  });

  return { list };
});

screen.dispose(); // `list` тоже распущен — его реакция отцеплена

Ребёнка по-прежнему можно распустить отдельно, родителя это не затронет. Любой dispose идемпотентен, поэтому распустить сначала ребёнка вручную, а потом родителя — безопасно.

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

Роспуск отменяет вызовы, сделанные моделью

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

ts
const loadUserFx = effect(async (id: string) => api.get(id)); // уровень модуля

const screen = owner(() => {
  void loadUserFx("u1"); // вызов принадлежит этому владельцу

  return {};
});

screen.dispose(); // запрос отменяется с Error("Effect caller disposed")

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

Внешняя очистка

Используйте onCleanup для работы, о которой Virentia не может знать сама.

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

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

  return {};
});

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

ts
import { onCleanup, owner, withOwner, type Owner } from "@virentia/core";

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

function connectSocket(modelOwner: Owner) {
  withOwner(modelOwner, () => {
    const socket = new WebSocket("/events");

    onCleanup(() => {
      socket.close();
    });
  });
}

connectSocket(model.owner);

Так вспомогательная функция остается переиспользуемой и не отвечает за весь жизненный цикл модели.

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