theater-flow
v0.1.2
Published
A library for building flows; safe data processing inspired by Monads
Maintainers
Readme
flow
Flow is a simple and easy effect manager for javascript and typescript. If you are struggling with runtime errors or if you want to add safety in your app, this library might interest you ℹ️
It's based on functional programming concept such as Monad to provides safe
and "error less" flow of execution for any apps.
Get Started
To install flow using your favourite package manager:
bun install theater-flowWhat is a flow
A flow is an execution safe zone. It means that any errors that can happens during a flow can be safely "catch" and represented in your app. Technically, it becomes possible to recover from any errors that can happens in your programms.
Basic Usage
Creating a Flow
A flow is a safe execution zone that handles both successful operations and errors in a predictable way. Here's how to create a basic flow:
import { flow, run } from "theater-flow";
// Create a simple flow that performs division
const divideFlow = flow((x: number, y: number) => {
if (y === 0) {
throw new Error("Division by zero is not allowed");
}
return x / y;
});Creating Multiple Flows
You can chain multiple operations in a flow using various operators. Here's an example that processes a string through multiple transformations:
import { flow, run, map } from "theater-flow";
const processString = flow(
// First operation: receive initial string
(input: string) => input.toLowerCase(),
// Map to transform the result
map((str) => str.replace(/\s+/g, "_")),
// Another transformation
map((str) => str.toUpperCase()),
// Final transformation
map((str) => `Processed: ${str}`)
);Running a Flow
To execute a flow, use the run function. Here's how to run flows and handle their results:
import { flow, run, isSuccess, isFailure } from "theater-flow";
const greetFlow = flow((name: string) => `Hello, ${name}!`);
async function example() {
// Run the flow
const result = await run(greetFlow, "John");
// The result will contain status, value, and cause properties
console.log(result);
// Output: { status: 'success', value: 'Hello, John!', cause: undefined }
}Retrieving Success Data
When a flow succeeds, you can access its value using the isSuccess helper:
import { flow, run, isSuccess } from "theater-flow";
const calculateFlow = flow((x: number, y: number) => x + y);
async function handleSuccess() {
const result = await run(calculateFlow, 5, 3);
if (isSuccess(result)) {
console.log("Success value:", result.value); // Output: Success value: 8
return result.value;
}
}Handling Errors
When a flow fails, you can access the error cause using the isFailure helper:
import { flow, run, isFailure } from "theater-flow";
const divideFlow = flow((x: number, y: number) => {
if (y === 0) {
throw new Error("Division by zero");
}
return x / y;
});
async function handleError() {
const result = await run(divideFlow, 10, 0);
if (isFailure(result)) {
console.log("Error message:", result.cause.message); // Output: Error message: Division by zero
// You can also access the full error cause
console.log("Error cause:", result.cause);
}
}The flow system automatically wraps errors in a structured format, making it easier to handle and recover from errors in a consistent way. The cause property contains detailed information about what went wrong, allowing you to make informed decisions about how to handle the error.
This basic usage covers the fundamental patterns for working with flows. The library provides additional operators like bind, recover, map, and others for more advanced use cases and error handling strategies.
Using Result
In Flow, everything revolves around the Result type - think of it as a box that can contain either success 🎉, failure 💥, or abort 🛑. Understanding these types is crucial for mastering Flow.
Anatomy of a Result
A Result can be one of three types:
// Success: Everything went according to plan! 🎉
type Success<T> = {
status: "success";
value: T;
cause: undefined;
};
// Failure: Oops, something went wrong! 💥
type Failure = {
status: "failure";
value: undefined;
cause: Error;
};
// Abort: "I'm gonna stop you right there" 🛑
type Abort = {
status: "abort";
value: undefined;
cause: undefined;
};Creating Results
You can create results using helper functions:
import { success, failure, abort } from "theater-flow";
// When your code is living its best life
const happyPath = success("I'm feeling good!");
// { status: 'success', value: "I'm feeling good!", cause: undefined }
// When your code had a rough day
const badDay = failure(new Error("I forgot my coffee ☕"));
// { status: 'failure', value: undefined, cause: Error("I forgot my coffee ☕") }
// When your code decides to take a break
const timeOut = abort();
// { status: 'abort', value: undefined, cause: undefined }Checking Result Types
Flow provides type guards to safely check what kind of result you're dealing with:
import { isSuccess, isFailure, isAbort } from "theater-flow";
function handleUserLogin(result: Result<User>) {
if (isSuccess(result)) {
console.log(`Welcome back, ${result.value.name}! 👋`);
return;
}
if (isFailure(result)) {
console.log(`Auth failed: ${result.cause.message} 😢`);
return;
}
if (isAbort(result)) {
console.log("Login process was cancelled. Did someone pull the plug? 🔌");
return;
}
}Working with Results in Practice
Here's a more realistic example showing how results flow through your application:
import { flow, run, map, recover } from "theater-flow";
const makeSandwich = flow(
async (ingredients: string[]) => {
if (ingredients.length === 0) {
throw new Error("Can't make a sandwich out of thin air! 🌫️");
}
if (!ingredients.includes("bread")) {
throw new Error("A sandwich without bread is just... sad toppings 😢");
}
return `A delicious sandwich with ${ingredients.join(", ")} 🥪`;
},
recover((error) => `Sandwich making failed: ${error.message}`),
map((result) => `${result} - Bon appétit! 🍽️`)
);
async function lunchTime() {
// Success case
const perfect = await run(makeSandwich, ["bread", "cheese", "ham"]);
// Result: "A delicious sandwich with bread, cheese, ham 🥪 - Bon appétit! 🍽️"
// Failure case (but recovered!)
const noIngredients = await run(makeSandwich, []);
// Result: "Sandwich making failed: Can't make a sandwich out of thin air! 🌫️ - Bon appétit! 🍽️"
// Another failure case (but recovered!)
const noBread = await run(makeSandwich, ["cheese", "ham"]);
// Result: "Sandwich making failed: A sandwich without bread is just... sad toppings 😢 - Bon appétit! 🍽️"
}Pro Tips for Working with Results
- Always handle all possible states (success, failure, abort) when checking results
- Use type guards (
isSuccess,isFailure,isAbort) for type-safe access to values and causes - Remember that
successcarries a value,failurecarries a cause, andabortcarries neither - Results are immutable - once created, they can't be changed (just like that sandwich you made... no takebacks!)
Now that you understand the Result type system, you're ready to dive into Flow's operators and create more complex flows! 🌊
Using Operators
Flow comes with a toolkit of operators that are like Swiss Army knives for your code - except they won't accidentally cut you when you put them in your pocket! 🔪 Let's meet our cast of characters:
The map Operator
Think of map as your code's personal makeover artist - it takes your successful values and gives them a fresh new look, while completely ignoring the failures (just like how we ignore our ex's social media posts 😅):
import { flow, run, map } from "theater-flow";
const makeItFancy = flow(
() => "hello",
map((value) => value.toUpperCase()), // Giving it the LOUD treatment
map(async (value) => value.length), // Counting its characters (async, because counting is hard)
map((value) => value * 2) // Double it, because why not?
);
// Will turn our humble "hello" into a whopping 10
// (5 letters * 2, for those who skipped math class)
const result = await run(makeItFancy);The bind Operator
bind is like a matchmaker for flows - it introduces one flow to another and hopes they'll hit it off. Perfect for when you want your flows to have a meaningful relationship 💑:
import { flow, run, bind } from "theater-flow";
const coffeeFlow = flow((type: string) => `${type} coffee`);
const addMilkFlow = flow((coffee: string) => `${coffee} with milk`);
const addSugarFlow = flow((coffee: string) => `${coffee} and sugar`);
const makeMyMorningBetter = flow(
() => "espresso",
bind(coffeeFlow), // First date with coffee
bind(addMilkFlow), // Getting milky
bind(addSugarFlow) // Sweet ending
);
// Result: "espresso coffee with milk and sugar" - Because adulting is hardThe recover Operator
recover is your code's safety net - like having a friend ready to catch you after a trust fall, except this friend actually shows up 🤸♂️:
import { flow, run, recover, map } from "theater-flow"
const divideLikeYouMeanIt = flow(
(x: number, y: number) => {
if (y === 0) throw new Error("Division by zero? Not today, Satan! 😈")
return x / y
},
recover(error => `Math said no: ${error.message}`),
map(result => `🧮 ${result}`)
)
// Will return "🧮 Math said no: Division by zero? Not today, Satan!"
const oops = await run(divideLikeYouMean It, 10, 0)The eff Operator
eff is like that friend who promises to keep a secret - it does something on the side but pretends nothing happened (perfect for logging, or sneaking cookies from the jar 🍪):
import { flow, run, eff, map } from "theater-flow";
const sneakyFlow = flow(
() => "super secret message",
eff(async (value) => {
console.log(`Don't tell anyone, but: ${value}`); // What happens in eff, stays in eff
}),
map((value) => value.toUpperCase())
);The rightMap and rightBind Operators
These are like the evil twins of map and bind - they only work on failures. Perfect for when you want to make your errors more... entertaining 😈:
import { flow, run, rightMap, rightBind } from "theater-flow";
const makeErrorsFun = flow(
() => {
throw new Error("boring error message");
},
rightMap((error) => `🎭 ${error.message}`),
rightBind(flow((msg) => `Why so serious? ${msg} 🃏`))
);To async or not to async? 🤔
Here's a little secret about Flow: it doesn't care whether your functions are async or not - it's like that friend who accepts you whether you're a morning person or a night owl! Let's see this in action:
import { flow, run, map } from "theater-flow";
const whoCaresAboutAsync = flow(
() => "I am sync", // Sync function? Cool! 😎
map(async (value) => `${value} but now async`), // Async function? Also cool! 🚀
map((value) => value.toUpperCase()), // Back to sync? No problem! 👍
map(async (value) => `${value}!!!`) // Async again? Whatever! 🤷♂️
);
// Flow handles all of these transitions seamlessly
const result = await run(whoCaresAboutAsync);
// Result: "I AM SYNC BUT NOW ASYNC!!!"But wait, there's more! Your flow functions can also return Result values directly. It's like ordering a pizza and getting it pre-sliced - sometimes you want to do the work yourself:
import { flow, run, success, failure, map } from "theater-flow";
const iKnowWhatImDoing = flow(
// Sync function returning a Result
(x: number) =>
x > 0
? success("Positive vibes only! ✨")
: failure(new Error("Why so negative? 😢")),
// Async function returning a Result
map(async (value) => {
if (value.includes("vibes")) {
return success("Groovy! 🕺");
}
return failure(new Error("Not groovy enough 😤"));
}),
// Mix and match as you please!
map((value) => success(`${value} Let's dance! 💃`))
);
// All these styles play nicely together
const goodMood = await run(iKnowWhatImDoing, 42);
// Result: { status: 'success', value: "Groovy! Let's dance! 💃", cause: undefined }
const badMood = await run(iKnowWhatImDoing, -1);
// Result: { status: 'failure', cause: Error("Why so negative? 😢"), value: undefined }Think of Flow as your code's personal assistant who:
- Doesn't judge your async/sync lifestyle choices 🧘♀️
- Handles all the Promise wrapping/unwrapping for you 🎁
- Lets you return Results directly when you're feeling confident 💪
- Makes everything work together like a well-choreographed dance routine 🕺💃
Remember: Whether you're going async, sync, or doing the Result-returning cha-cha, Flow's got your back! It's like having a very supportive friend who's also really good at organizing your chaos. 🌪️➡️✨
Pro Tips for Operator Mastery
- Mix and match operators like you're making a cocktail 🍸 - but remember, too many operators might give your code a hangover
- Always handle your errors - they have feelings too!
- When in doubt,
mapit out bindresponsibly - not every flow needs to be chained together (we're writing code, not making paperclips)- Use
effwhen you want to be sneaky, but remember: with great power comes great responsibility 🕷️
Remember, these operators are your friends (except when they're not, but that's probably your fault). Use them wisely, and they'll turn your error-prone code into a well-oiled machine that practically runs itself!
Now go forth and flow responsibly! 🌊
