Всё в модели 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завершаются в одном запуске, поэтому хранилище, объединяющее оба источника, обновляется один раз, а не два.