Перейти к содержанию
Mercury
Русский
Дизайн книга
Меню
Русский

Единицы: store, event и effect

Модель состоит из трёх видов единиц Hypergraph (unit), названных по видам, записанным в графе. Хранилище (store) содержит значение, событие (event) передаёт данные, а эффект (effect) запускает обработчик и завершается.

Всё в модели Hypergraph — единицы, и каждая относится к одному из трёх видов. Для каждого вида есть функция, импортируемая из @fiberfx/hypergraph, а каждый дескриптор содержит свой вид в .kind.

Три вида

Вид Создаётся через Что хранит или передаёт Можно вызвать?
store store(init, config?) значение, начиная с init и до обновления нет
event event<T>(config?) данные, один раз на вызов да, с данными
effect effect(handler) выполнение обработчика и его результат да, возвращает промис

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

store

store(init, config?) создаёт изменяемое хранилище (store), содержащее init до первого обновления. В настройках можно задать:

  • name: имя, которое разработчик видит в отчёте;
  • sid: идентификатор, заданный вручную вместо назначенного компилятором, если значение нужно повторно находить после правки или при серверном рендеринге;
  • skipVoid: пропускать ли обновление до undefined или сохранять его;
  • updateFilter(next, prev): определяет, принимается ли обновление;
  • serialize: 'ignore', чтобы значение никогда не передавалось в кадре состояния (frame), либо {write, read} для значения, которому требуется собственное представление;
  • schema: {id, version}, объявленная схема; описание структуры (shape) хранилища принимает вид <id>@<version>.

Изменяемое хранилище обновляется через .on(clock, fn), где clock — единица или список единиц, а fn(state, payload) возвращает следующее состояние. .reset(...clocks) возвращает его к init. Оба метода возвращают хранилище, поэтому вызовы можно объединять в цепочку.

У каждого хранилища, изменяемого или производного, есть:

  • .getState(): состояние в области состояния (scope) вызывающей стороны — наблюдателя, обработчика или привязанного вызова; иначе состояние вне какой-либо области;
  • .map(fn, {skipVoid?}): производное хранилище;
  • .updates: событие его обновлений;
  • .watch(fn): вызывается с текущим состоянием, а затем при каждом обновлении.
import { event, store } from "@fiberfx/hypergraph";

const increment = event<number>();
const reset = event();

export const $count = store(0, { name: "$count" })
  .on(increment, (count, by) => count + by)
  .reset(reset);

export const $doubled = $count.map((count) => count * 2);

setState и off не входят в API. Хранилище меняется только через свои связи.

event

event<T>(config?) создаёт вызываемое событие (event); в настройках можно задать name. Вызов с данными запускает одно обновление через всё, что связано с событием.

  • .map(fn), .filter(fn) и .filterMap(fn) создают производные события. filter также принимает {fn}; filterMap отбрасывает данные, для которых fn возвращает undefined.
  • .prepend(fn) вызываемого события создаёт новое вызываемое событие, данные которого сначала проходят через fn.
  • .watch(fn) вызывается с каждой порцией данных.
import { event } from "@fiberfx/hypergraph";

export const typed = event<string>();
export const trimmed = typed.map((text) => text.trim());
export const nonEmpty = trimmed.filter((text) => text.length > 0);
export const fromNumber = typed.prepend((n: number) => String(n));

effect

effect(handler) создаёт эффект (effect) из обработчика, который может вернуть значение или промис. Вызов эффекта запускает обработчик и возвращает промис его результата. Результат также поступает через единицы:

  • .done передаёт {params, result}, а .doneData — результат;
  • .fail передаёт {params, error}, а .failData — ошибку;
  • .finally передаёт {status: 'done', params, result} или {status: 'fail', params, error};
  • .pending — хранилище признака выполняющегося вызова, а .inFlight — хранилище числа таких вызовов.

У эффекта также есть методы события (map, filter, filterMap, prepend, watch). Эффект без обработчика завершается ошибкой при вызове, пока handlers области состояния не предоставят ему обработчик (области состояния). Метода .use нет: обработчик задаётся один раз при создании или отдельно для области состояния.

import { effect, store } from "@fiberfx/hypergraph";

export const loadUser = effect(async (id: number) => {
  const response = await fetch(`/api/users/${id}`);
  return (await response.json()) as { name: string };
});

export const $name = store("").on(loadUser.doneData, (_, user) => user.name);
export const $loading = loadUser.pending;

Декларативные эффекты

effect принимает вместо обработчика и декларативное описание: {request}, {channel, payload?} или {timer}. Описание — это данные, которые выполняет исполнитель области состояния, поэтому тот же эффект может исполняться и вне JavaScript. Слот содержит либо функцию JavaScript, что помечает эффект как доступный только в JavaScript, либо выражение hgx/1.

Это предварительная версия. Исполнители появятся на более позднем этапе (H4). Пока область не создана с performers, декларативный эффект выбрасывает ошибку об отсутствии подходящего исполнителя в области.

Что фиксируют тестовые векторы

Векторы проверки соответствия описывают поведение данными; каждая среда выполнения запускает их:

  • store.on-void-skips: без явно заданного skipVoid редьюсер, вернувший undefined, оставляет хранилище прежним и не вызывает обновления. store.on-void-value-allowed описывает случай skipVoid: false, когда undefined принимается.
  • effect.pending-false-true-false: наблюдатель .pending видит false (текущее состояние), true при начале вызова и false при его завершении.
  • effect.finally-done-payload: .finally передаёт {status: "done", params, result}.
  • effect.done-and-finally-one-launch: .done и .finally завершаются в одном запуске, поэтому хранилище, объединяющее оба источника, обновляется один раз, а не два.