@vr1/flow-r
v4.0.0
Published
Утилита для конвейерно ориентированного программирования. Подобно монаде Either, даёт обработку ошибок без исключений, сквозную передачу параметров, каскадирование.
Readme
@vr1/flow-r
Утилита для конвейерно ориентированного программирования. Подобно монаде Either, даёт обработку ошибок без исключений. А также добавляет сквозную передачу параметров и каскадирование. Позволяет создавать как синхронные так и асинхронные цепочки/рецепты.
Описание
@vr1/flow-r — это библиотека для построения программ с явным и удобным управлением потоком выполнения, обработкой ошибок без использования исключений (throw/catch), и сквозной передачей параметров. Вдохновлена монадами типа Either/Result.
Позволяет организовывать цепочки вызовов («конвейеры») с автоматическим распространением ошибок и удобным контролем над потоком данных.
Установка
npm install @vr1/flow-rОсновные концепции
- Ошибки без исключений — обработка ошибок происходит явно через возвращаемые значения, без выбрасывания исключений.
- Поток данных — параметры и ошибки передаются через весь пайплайн без лишней кухонной логики.
- Каскадирование — рецепты могут использоваться как шаги для вышестоящих рецептов.
Примеры использования
Базовый пример - путь
import { путь } from "@vr1/flow-r";
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
// результирующая функция будет возвращать весь набор параметров, проходящий через цепочку
}
// Завершить цепочку нужно методом выход, и указать набор полей.
const результирующая_функция
= путь("результирующая_функция")
.шаг(действие1)
.шаг(действие2)
.шаг(действие3)
.выход("поле3");
// сигнатура результирующей функции станет такой
type РезультирующаяФункция = (вход: { ключ1: string, ключ2: number }) => { поле3: string } | Ошибка;Обёртка для пути
Иногда нужно выполнить какой-либо код до и после выполнения всей цепочки шагов. Например, можно использовать обёртку для управления транзакциями в базе данных. Для этого используется метод .обертка().
Обёртка — это функция, которая получает входные данные и callback-функцию для выполнения цепочки шагов. Callback должна быть вызвана, чтобы выполнить остальную цепочку.
import { путь } from "@vr1/flow-r";
async function в_транзакции(вход: { id: number }, вызов: () => Promise<void>): Promise<void> {
const транзакция = await начни_транзакцию();
const результат = await вызов(); // Выполняет все шаги цепочки внутри транзакции
if (это_ошибка(результат)) return await транзакция.rollback();
await транзакция.commit();
}
const цепочка =
путь("сохрани_в_бд")
.обертка(в_транзакции)
.шаг(обновить_корзину)
.шаг(обновить пользователя)
.выход("статус");
await цепочка({ id: 10, ... });Важные особенности обёртки:
- Обёртка получает входные данные — в первый параметр обёртка получит данные с которыми вызвана цепочка, нужно указать те, что нужны обёртке.
- Обёртка должна вызвать callback — функция
вызов()запускает выполнение всей цепочки шагов. - Callback возвращает результат — обёртка может получить результат от
вызов()и проверить, является ли он ошибкой, используя функциюэто_ошибка(). Это полезно для обработки ошибок на уровне обёртки. - Поддерживает асинхронность — обёртка может быть асинхронной функцией, и callback также может возвращать Promise.
- Обёртка может возвращать ошибку — если обёртка возвращает ошибку, то цепочка вернёт эту ошибку, если обёртка не возвращает ничего, то цепочка вернёт то, что возвращают шаги.
Асинхронное выполнение
Путь поддерживает как синхронные шаги, так и асинхронные. Если все шаги синхронные, то и результирующая функция будет синхронной. Если же хоть один из шагов будет асинхронным, то и результирующая функция будет асинхронной.
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<void | Ошибка>;
async function пример() {
await отправь_письмо_пользователю({ ключ_пользователя: "Пользователь123", содержимое_письма: "здравствуйте, бла-бла" });
}
Для подобных методов, как правило, не нужно, чтобы они что-либо возвращали. Поэтому, чтобы не засорять контекст вызывающей функции можно использовать пустой возврат.
const отправь_письмо_пользователю =
путь("отправь_письмо_пользователю")
.шаг(загрузи_пользователя)
.шаг(почтовый_адрес_пользователя)
.шаг(отправь_письмо)
.выход();
// автоматически будет иметь тип:
// (вход: { ключ_пользователя: string, содержимое_письма: string }) => Promise<void | Ошибка>;Обработка ошибок
Как и монада 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 для добавления параллельных действий в охват. |
| выход | Зарезервировано как метод 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
