Асинхронность
Простыми словами
Как загружать данные, показывать состояния «грузится» и «ошибка» и никогда не ловить гонку ответов.
Сеть FRP чистая и синхронная: момент закрывается, эффекты — после него. Асинхронный мир входит и выходит через два люка.
perform: IO как данные
perform превращает событие запросов в событие результатов. Промис исполняется после закрытия момента; его результат открывает новый момент. Ошибка — не исключение, а ветка Result:
import { perform, type Result } from "@continuum-js/frp";
const results = perform(saveClicks, (draft) => api.save(draft));
// Stream<Result<unknown, SaveResponse>>
const saved = results.filter((r) => r.ok);
const failed = results.filter((r) => !r.ok);Обе ветки — обычные события: их можно merge, копить accum, показывать через hold.
resource: конечный автомат загрузки
Для типового «загрузи по триггеру и покажи» есть resource из @continuum-js/std:
import { resource } from "@continuum-js/std";
import { Dynamic } from "@continuum-js/dom";
const state = resource(userId.updates, (id) => api.fetchUser(id));
// State<Async<User>>: { status: "idle" | "loading" | "ok" | "error" }
<Dynamic value={state}>
{(s) =>
s.status === "loading" ? (
<Spinner />
) : s.status === "error" ? (
<ErrorBox error={s.error} />
) : s.status === "ok" ? (
<Profile user={s.value} />
) : null
}
</Dynamic>;Ключевое: гонка ответов решена внутри. Запросы нумеруются; ответ на устаревший запрос игнорируется (last-request-wins). Классический баг «быстрый ответ на старый запрос перетёр свежий» не воспроизводим по построению — в React это ручной cancelled-флажок в каждом useEffect.
Debounce-поиск: композиция вместо хука
Комбинаторы событий и resource собираются как конструктор:
import { debounce } from "@continuum-js/std";
const settled = debounce(query.updates, 300); // Stream<string>
const results = resource(settled, (q) => api.search(q));Ни кастомного хука, ни таймеров в компоненте: debounce — чистый оператор над событием, resource — автомат над результатами.
Таймеры
interval(ms) из std — событие тиков; delay(e, ms) — сдвиг события во времени. Их таймеры останавливаются при dispose() события — в компоненте привяжите это к дереву владения явно:
import { interval } from "@continuum-js/std";
import { onCleanup } from "@continuum-js/dom";
const ticks = interval(1000);
onCleanup(() => ticks.dispose());Для анимаций есть animationFrames() из dom — он регистрируется во владельце сам — и непрерывное время (integral/warp) из ядра; см. гайд по анимации.
Правило одного направления
Асинхронный код никогда не «дописывает» состояние сам — он всегда порождает событие, которое сеть сворачивает обычными средствами (hold, accum, resource). Если хочется вызвать set из .then() — скорее всего, это место для perform.
Незнакомый термин? Вся терминология этой документации объяснена в глоссарии.