@vr1/flow-r
v2.0.6
Published
Утилита для конвейерно ориентированного программирования. Подобно монаде Either, даёт обработку ошибок без исключений, сквозную передачу параметров, каскадирование.
Readme
@vr1/flow-r
Утилита для конвейерно ориентированного программирования. Подобно монаде Either, даёт обработку ошибок без исключений. А также добавляет сквозную передачу параметров и каскадирование. Позволяет создавать как синхронные так и асинхронные цепочки/рецепты.
Описание
@vr1/flow-r — это библиотека для построения программ с явным и удобным управлением потоком выполнения, обработкой ошибок без использования исключений (throw/catch), и сквозной передачей параметров. Вдохновлена монадами типа Either/Result.
Позволяет организовывать цепочки вызовов («конвейеры») с автоматическим распространением ошибок и удобным контролем над потоком данных.
Установка
npm install @vr1/flow-rОсновные концепции
- Ошибки без исключений — обработка ошибок происходит явно через возвращаемые значения, без выбрасывания исключений.
- Поток данных — параметры и ошибки передаются через весь пайплайн без лишней кухонной логики.
- Каскадирование — рецепты могут использоваться как шаги для вышестоящих рецептов.
Примеры использования
Базовый пример - путь
import { путь } from "@vr1/flow-r";
const результирующая_функция
= путь("результирующая_функция")
.шаг(действие1)
.шаг(действие2)
.шаг(действие3);
function действие1(вход: { ключ1: string }): { объект1: Object } {
// результирующая функция потребует на вход параметр ключ1
// в виде { ключ1: string }
}
function действие2(вход: { ключ2: number, объект1: Object }): { объект2: Map<string, string> } {
// результирующая функция потребует на вход параметр ключ2
// в дополнение к ключ1, результирующий входной параметр будет { ключ1: string, ключ2: number }
// несмотря на то, что объект1 тут требуется на вход, в результирующую функцию на вход его подавать не надо
// действие2 на вход его получит из результата действия1
}
function действие3(вход: { объект1: Object, объект2: Map<string, string> }): { поле3: string } {
// действие3 получит на вход тот объект1, что вернёт действие1, и тот объект2, что вернёт действие2
// результирующая функция будет возвращать весь набор параметров, проходящий через цепочку
}
// сигнатура результирующей функции автоматически выведется тайпскриптом как следующее
type РезультирующаяФункция = (вход: { ключ1: string, ключ2: number }) => { объект1: Object, объект2: Map<string, string>, поле3: string } | Ошибка;
Если требуется ограничить выходной набор полей только нужным полем, то нужно завершить цепочку действием выход
const результирующая_функция
= путь("результирующая_функция")
.шаг(действие1)
.шаг(действие2)
.шаг(действие3)
.выход(({ поле3 }) => ({ поле3 }));
// сигнатура результирующей функции станет такой
type РезультирующаяФункция = (вход: { ключ1: string, ключ2: number }) => { поле3: string } | Ошибка;Второй вариант приведёт к аналогичному результату
const результирующая_функция
= путь("результирующая_функция")
.шаг(действие1)
.шаг(действие2)
.шаг(действие3)
.выход("поле3");Асинхронное выполнение
Путь поддерживает как синхронные шаги, так и асинхронные. Если все шаги синхронные, то и результирующая функция будет синхронной. Если же хоть один из шагов будет асинхронным, то и результирующая функция будет асинхронной.
import { путь } from "@vr1/flow-r";
async function загрузить_пользователя(вход: { ключ_пользователя: string }): { пользователь: Пользователь } {
// например, асинхронное получение id пользователя по имени
return { пользователь: await /* код, работающий с БД, например */ };
}
function почтовый_адрес_пользователя(вход: { пользователь: Пользователь }): { email: string } {
// синхронный шаг, возвращает email по id
return { email: `user${вход.ключ_пользователя}@mail.com` };
}
async function отправить_письмо(вход: { email: string, содержимое_письма: string }): Promise<void> {
await /* код, работающий с email */
}
const отправить_письмо_пользователю =
путь("отправить_письмо_пользователю")
.шаг(загрузить_пользователя)
.шаг(почтовый_адрес_пользователя)
.шаг(отправить_письмо);
// автоматически будет иметь тип:
// (вход: { ключ_пользователя: string, содержимое_письма: string }) => Promise<{ пользователь: Пользователь, email: string } | Ошибка>;
async function пример() {
await отправить_письмо_пользователю({ ключ_пользователя: "Пользователь123", содержимое_письма: "здравствуйте, бла-бла" });
}
Для подобных методов, как правило, не нужно, чтобы они что-либо возвращали. Поэтому, чтобы не засорять контекст вызывающей функции можно использовать пустой возврат.
const отправить_письмо_пользователю =
путь("отправить_письмо_пользователю")
.шаг(загрузить_пользователя)
.шаг(почтовый_адрес_пользователя)
.шаг(отправить_письмо)
.выход();
// автоматически будет иметь тип:
// (вход: { ключ_пользователя: string, содержимое_письма: string }) => Promise<{} | Ошибка>;Обработка ошибок
Как и монада Either, flow-r работает с ошибками, избегая исключений. Любой из шагов цепочки может вернуть ошибку, и тогда выполнение всей цепочки прервётся и функция, представляющая собой цепочку сразу вернёт эту ошибку.
Параллельное выполнение - охват
Иногда нужно выполнить несколько независимых действий параллельно и объединить их результаты. Для этого используется функция и (или охват для именованного варианта).
import { и, путь } from "@vr1/flow-r";
function загрузить_профиль(вход: { id_пользователя: number }): { профиль: Профиль } {
// загрузка профиля из БД
return { профиль: /* ... */ };
}
function загрузить_настройки(вход: { id_пользователя: number }): { настройки: Настройки } {
// загрузка настроек из БД
return { настройки: /* ... */ };
}
function загрузить_уведомления(вход: { id_пользователя: number }): { уведомления: Уведомление[] } {
// загрузка уведомлений из БД
return { уведомления: /* ... */ };
}
// Все три функции выполнятся параллельно, результаты объединятся
const загрузить_данные_пользователя =
и(загрузить_профиль)
.и(загрузить_настройки)
.и(загрузить_уведомления);
// Тип результата автоматически выведется как:
// (вход: { id_пользователя: number }) => { профиль: Профиль, настройки: Настройки, уведомления: Уведомление[] } | ОшибкаЕсли любая из параллельных функций вернёт ошибку, весь охват вернёт ошибку с информацией обо всех ошибках.
Охват можно использовать как шаг в пути:
const показать_дашборд =
путь("показать_дашборд")
.шаг(и(загрузить_профиль).и(загрузить_настройки).и(загрузить_уведомления))
.шаг(отрендерить_дашборд);Ограничение выхода работает аналогично пути:
const загрузить_только_профиль =
и(загрузить_профиль)
.и(загрузить_настройки)
.выход("профиль");
// или через селектор
const загрузить_только_профиль_2 =
и(загрузить_профиль)
.и(загрузить_настройки)
.выход(({ профиль }) => ({ профиль }));Условное выполнение - если
Для выполнения шага только при выполнении условия используется функция если. Предикат должен возвращать объект { условие: boolean }.
import { если, путь } from "@vr1/flow-r";
function пользователь_премиум(вход: { пользователь: Пользователь }): { условие: boolean } {
return { условие: вход.пользователь.тариф === "премиум" };
}
function загрузить_премиум_контент(вход: { пользователь: Пользователь }): { контент: Контент } {
return { контент: /* премиум контент */ };
}
function загрузить_базовый_контент(вход: { пользователь: Пользователь }): { контент: Контент } {
return { контент: /* базовый контент */ };
}
// Только то (без иначе) — если условие false, результат будет undefined
const загрузить_бонус =
если(пользователь_премиум)
.то(загрузить_премиум_контент);
// С веткой иначе — всегда выполнится один из вариантов
const загрузить_контент_по_тарифу =
если(пользователь_премиум)
.то(загрузить_премиум_контент)
.иначе(загрузить_базовый_контент);Использование в пути:
const показать_контент =
путь("показать_контент")
.шаг(загрузить_пользователя)
.шаг(
если(пользователь_премиум)
.то(загрузить_премиум_контент)
.иначе(загрузить_базовый_контент)
)
.шаг(отобразить_контент);Прерывание по условию - ошибка_если
Функция ошибка_если позволяет прервать выполнение цепочки, если выполняется определённое условие. Полезно для валидации входных данных или проверки бизнес-правил.
import { ошибка_если, путь } from "@vr1/flow-r";
function пользователь_заблокирован(вход: { пользователь: Пользователь }): { условие: boolean } {
return { условие: вход.пользователь.статус === "заблокирован" };
}
function баланс_недостаточен(вход: { счёт: Счёт, сумма: number }): { условие: boolean } {
return { условие: вход.счёт.баланс < вход.сумма };
}
// Если условие true — цепочка прервётся с ошибкой
const выполнить_перевод =
путь("выполнить_перевод")
.шаг(загрузить_пользователя)
.шаг(ошибка_если(пользователь_заблокирован, "Пользователь заблокирован"))
.шаг(загрузить_счёт)
.шаг(ошибка_если(баланс_недостаточен, "Недостаточно средств на счёте"))
.шаг(провести_транзакцию);Если пользователь_заблокирован вернёт { условие: true }, то функция выполнить_перевод сразу вернёт ошибку с текстом "Пользователь заблокирован", а шаги загрузить_счёт и провести_транзакцию не будут выполнены.
Второй параметр (текст ошибки) опционален:
// Без указания текста ошибки будет использован текст по умолчанию
.шаг(ошибка_если(пользователь_заблокирован))Ограничения
Зарезервированные поля
Некоторые имена полей зарезервированы библиотекой и не должны использоваться во входных и выходных объектах ваших функций:
| Поле | Причина |
|------|---------|
| __ошибка | Используется внутри библиотеки для маркировки объектов ошибок. Если ваш объект содержит это поле, он будет воспринят как ошибка и прервёт выполнение цепочки. |
| условие | Зарезервировано для предикатов (если, ошибка_если). Предикаты обязаны возвращать объект вида { условие: boolean }. Использование этого поля в обычных шагах может привести к конфликтам. |
| шаг | Зарезервировано как метод fluent API для добавления шагов в путь. |
| и | Зарезервировано как метод fluent API для добавления параллельных действий в охват. |
| выход | Зарезервировано как метод fluent API для ограничения выходных полей. |
// ❌ Плохо — поле __ошибка зарезервировано
function плохой_шаг(вход: { данные: string }): { __ошибка: string } {
return { __ошибка: "что-то" }; // будет воспринято как ошибка!
}
// ❌ Плохо — поле условие зарезервировано для предикатов
function плохой_шаг2(вход: { x: number }): { условие: string } {
return { условие: "готово" }; // конфликт с типом предиката
}
// ❌ Плохо — поля шаг, и, выход зарезервированы для fluent API
function плохой_шаг3(вход: { x: number }): { шаг: number, и: string, выход: boolean } {
return { шаг: 1, и: "что-то", выход: true }; // конфликт с методами API
}
// ✅ Хорошо — используйте другие имена полей
function хороший_шаг(вход: { данные: string }): { результат: string } {
return { результат: "готово" };
}Лицензия
MIT
