Skip to content

efflore/flow-sure

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FlowSure

Version 0.10.0

Inspired by functional programming, FlowSure provides tools like ok(), maybe(), result(), task() and flow() to simplify complex workflows. Here's a quick example:

import { ok, result } from "@efflore/flow-sure";

ok(5)
    .map(x => x * 2)
    .filter(x => x > 5)
    .match({
        Ok: value => console.log("Success:", value),
        Nil: () => console.warn("No value passed the filter"),
        Err: error => console.error("Error:", error.message),
    });

result(() => JSON.parse("invalid json"))
    .match({
        Ok: value => console.log("Parsed:", value),
        Err: error => console.error("Failed to parse:", error.message),
    });

Key Features

  • Simplify Error Handling: Capture and propagate errors elegantly with Result types.
  • Composable Flows: Chain both sync and async functions seamlessly using monadic methods.
  • Declarative Control: Use flow() to build clear, predictable pipelines.
  • Immutability Assurance: Safely wrap and clone mutable objects to avoid unintended side effects.

FlowSure is very lightweight: around 1kB gzipped.

Installation

# with npm
npm install @efflore/flow-sure

# or with bun
bun add @efflore/flow-sure

Basic Usage

Monadic Control with Result Types

FlowSure's Result types (Ok, Err, Nil) let you manage value transformations and error handling with .map(), .chain(), .filter(), .guard(), .or() and .catch() methods. The match() method allows for pattern matching according to the Resulttype. Finally, get() will return the value (if Ok) or undefined (if Nil) or rethow the catched Error (if Err).

import { ok } from "@efflore/flow-sure";

ok(5).map(x => x * 2).filter(x => x > 5).match({
    Ok: value => console.log("Transformed Value:", value),
    Nil: () => console.warn("Value didn't meet filter criteria"),
    Err: error => console.error("Error:", error.message)
});

Monadic Methods Table

Method Ok<T> Nil Err<E extends Error> Argument Type Return Type
.map() Yes No-op No-op (value: T) => U Ok<U>, Nil or Err<E>
.chain() Yes No-op No-op (value: T) => Result<U> Result<U>
.await() Yes No-op No-op async (value: T) => Result<U> Promise<Result<U>>
.filter() Yes No-op Converts to Nil (value: T) => boolean Maybe<T>
.guard() Yes No-op Converts to Nil (value: T) => value is U Maybe<T>
.or() No-op Yes Yes () => T | undefined Ok<T> or Maybe<T>
.catch() No-op No-op Yes (error: E) => Result<T> Result<T>
.match() Yes Yes Yes (value: T) => any, () => any or (error: E) => any any
.get() Returns value Returns undefined Throws error -- T, void or E

Explanation of Each Method

  • .map(): Transforms the value if it exists (Ok); Nil and Err remain unchanged.
  • .chain(): Chains a function that returns a new Result, only applies to Ok.
  • .await(): Chains an async function that returns a Promise for a new Result, only applies to Ok.
  • .filter(): Filters Ok based on a condition; converts to Nil if the condition is not met.
  • .guard(): Similar to .filter(), but specifically used for type narrowing on Ok.
  • .or(): Provides a fallback for Nil and Err, leaving Ok unchanged.
  • .catch(): Handles errors within Err types, leaving Ok and Nil unchanged.
  • .match(): Allows pattern matching across Ok, Nil, and Err.
  • .get(): Retrieves the contained value, returning undefined for Nil and throwing for Err.

Handling Optional or Missing Values with maybe()

Using maybe() ensures that undefined or null values are handled explicitly, reducing the risk of runtime errors caused by forgotten null checks. Instead of if statements scattered throughout your code, you can use methods like .map(), .filter(), and .match() to express intent clearly.

import { maybe } from "@efflore/flow-sure";

const optionalValue = undefined; // Could also be null or an actual value
maybe(optionalValue)
    .map(value => value * 2)
    .filter(value => value > 5)
    .match({
        Ok: value => console.log("Value:", value),
        Nil: () => console.warn("Value is either missing or didn't meet criteria")
    });

Handling Exceptions with result()

result() is especially useful for wrapping code that may throw unexpected errors, such as parsing user input or accessing properties on potentially null objects.

It captures exceptions and converts them into Err values, allowing you to handle errors gracefully within the chain.

import { result } from "@efflore/flow-sure";

result(() => {
    // Function that may throw an error
    return JSON.parse("invalid json");
}).match({
    Ok: value => console.log("Parsed JSON:", value),
    Err: error => console.error("Failed to parse JSON:", error.message)
});

Handling Promises with task()

Use task() to retrieve and handle a promised result, wrapping it in Result types (Ok, Err, Nil). Here's an example of how you can add retry logic for async operations:

import { task, err } from "@efflore/flow-sure";

const fetchData = async () => {
    const response = await fetch('/api/data');
    if (!response.ok) return err(`Failed to fetch data: ${response.statusText}`);
    return response.json();
}

const retry = <T>(
    fn: () => Promise<MaybeResult<T>>,
    retries: number,
    delay: number
) => task(fn).catch((error: Error) => {
        if (retries <= 0) return err(error);
        return new Promise(resolve => setTimeout(resolve, delay))
            .then(() => retry(fn, retries - 1, delay * 2));
    });

// 3 attempts, exponential backoff with initial 1000ms delay
const loadData = async () {
	const data = await retry(fetchData, 3, 1000);
	// Process data ...
}

Using flow() for Declarative Control

flow() enables you to compose a series of functions (both sync and async) into a cohesive pipeline:

import { flow } from "@efflore/flow-sure";

const processData = async () {
	const result = await flow(
		10,
		x => x * 2,
		async x => await someAsyncOperation(x)
	).match({
		Ok: finalValue => console.log("Result:", finalValue),
		Err: error => console.error("Error:", error.message)
	});
	// Render data ...
}

Using ok() for Immutability Guarantees

The ok() function wraps values to enforce immutability and prevent multiple retrievals. For mutable objects, it attempts to clone the value using structuredClone(). This ensures that modifications to the original object do not affect the wrapped value.

Note: Some types (e.g., DOM elements, promises, WeakMap, WeakSet) cannot be cloned due to structuredClone() limitations. In these cases, ok() falls back to treating the value as immutable without guarantees against external modification. Document these edge cases in your codebase if immutability is critical.

The value of an Ok instance can only be retrieved once using .get(). Any subsequent attempts will throw a ReferenceError. You can check if the value has already been consumed using isGone() or the .gone property on the instance.

import { ok } from '@efflore/flow-sure';

// Example with a mutable object
const original = { a: 1, b: 2 };
const wrapped = ok(original);

// Modifying the original object does not affect the wrapped value
original.a = 42;
console.log(wrapped.get()); // { a: 1, b: 2 } (immutable clone)

// Attempting to retrieve the value again throws a ReferenceError
try {
    console.log(wrapped.get());
} catch (e) {
    console.error(e.message); // "Mutable reference has already been consumed"
}

// Check if the value has been consumed
console.log(wrapped.gone); // true

Exported Helper Functions

FlowSure also exports the following utility functions it uses internally:

isFunction(value: unknown): value is (...args: any[]) => any

Checks if the given value is a function.

isAsyncFunction(value: unknown): value is (...args: any[]) => Promise<any>

Checks if the given value is an asynchronous function (returns a Promise).

isDefined(value: unknown): value is NonNullable<typeof value>

Checks if the given value is neither null nor undefined.

isMutable(value: unknown): value is Record<PropertyKey, unknown>

Checks if the given value is a mutable object (non-null and typeof value === "object").

isInstanceOf<T>(type: new (...args: any[]) => T): (value: unknown) => value is T

Creates a type guard to check if a value is an instance of a specific class or type.

isError(value: unknown): value is Error

Checks if the given value is an Error instance.

log(msg: string, logger: (...args: any[]) => void = console.log): (...args: any[]) => any

Logs a message and additional arguments using the specified logger (default: console.log). Returns the first argument for chaining.

tryClone<T>(value: T, warn = true): T

Attempts to clone a mutable object using structuredClone(). If cloning fails, logs a warning (if warn is true) and returns the original value.

wrap<T>(value: MaybeResult<T>): Result<T>

Wraps a value in a Result container. If the value is an error, it returns Err. If the value is undefined or null, it returns Nil. Otherwise, it returns Ok. Values that are already of a Result type are not double-wrapped.

unwrap<T>(value: Result<T> | T | void): T | Error | void

Unwraps a Result container, returning the value if it is Ok, the error if it is Err, or undefined if it is Nil.

About

Result data type (Ok, Err, Nil)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published