Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,27 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
# Sync (rebase) beta branch with the latest main changes
- name: Sync beta branch with main
if: github.ref == 'refs/heads/main'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

# Ensure we have the latest changes from main and beta branches
git fetch origin main beta

# Configure remote URL to use the GitHub token for authentication
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git

echo "🔄 Rebasing beta branch with main"
if git checkout beta && git rebase origin/main; then
git push origin beta --force
echo "✅ Beta branch updated successfully"
else
echo "❌ Failed to update beta branch"
git rebase --abort || true
exit 1
fi
81 changes: 79 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This is a set of implementations of monads in TypeScript with OOP perspective.
* [Mapping over an Either](#mapping-over-an-either)
* [Using `flatMap` and `flatMapLeft`](#using-flatmap-and-flatmapleft)
* [Using `map` and `mapLeft`](#using-map-and-mapleft)
* [Using Railway Pattern Methods](#using-railway-pattern-methods)
* [Recovering from a Left value](#recovering-from-a-left-value)
* [Running side effects](#running-side-effects)
* [Folding an Either](#folding-an-either)
Expand All @@ -21,6 +22,7 @@ This is a set of implementations of monads in TypeScript with OOP perspective.
* [Asynchronous Operations (AsyncEither)](#asynchronous-operations-asynceither)
* [Creating an AsyncEither](#creating-an-asynceither)
* [Mapping over an AsyncEither](#mapping-over-an-asynceither)
* [Using Railway Pattern Methods with AsyncEither](#using-railway-pattern-methods-with-asynceither)
* [Running side effects](#running-side-effects-1)
* [Folding an AsyncEither](#folding-an-asynceither)
* [Working with Promises](#working-with-promises)
Expand Down Expand Up @@ -119,8 +121,6 @@ transform the value inside a `Left`.
```typescript
import { Either } from '@leanmind/monads';

m

const right = Either.right(42).flatMap(x => Either.right(x + 1)); // Right(43)
const left = Either.left('Error').flatMapLeft(err => Either.left(`New ${err}`)); // Left('New Error')
```
Expand All @@ -134,6 +134,32 @@ const right = Either.right(42).map(x => x + 1); // Right(43)
const left = Either.left('Error').mapLeft(err => `New ${err}`); // Left('New Error')
```

#### Using Railway Pattern Methods

You can use `andThen` and `orElse` methods which follow the Railway-oriented programming pattern. These methods are semantically equivalent to `flatMap` and `flatMapLeft` but offer more readable syntax for error handling flows.

```typescript
import { Either } from '@leanmind/monads';

// Using andThen to chain operations on successful values (Right)
const right = Either.right(42)
.andThen(x => Either.right(x + 1)); // Right(43)

// Using orElse to handle errors (Left)
const left = Either.left('Error')
.orElse(err => Either.left(`Handled: ${err}`)); // Left('Handled: Error')

// Chaining operations with Railway methods
const result = Either.right(42)
.andThen(x => {
if (x > 40) {
return Either.right(x + 1);
}
return Either.left('Value too small');
})
.orElse(err => Either.left(`Error: ${err}`)); // Right(43)
```

#### Recovering from a Left value

You can use the `recover` method to recover from a `Left` value and transform it into a `Right`.
Expand Down Expand Up @@ -325,6 +351,57 @@ const asyncMapped = await AsyncEither.fromSync(Either.right(42))
}); // AsyncEither<never, number>
```

##### Using Railway Pattern Methods with AsyncEither

Similar to synchronous Either, AsyncEither also supports Railway-oriented programming with `andThen` and `orElse` methods:

```typescript
import { AsyncEither, Either } from '@leanmind/monads';

// Using andThen with AsyncEither
const result = await AsyncEither.fromSync(Either.right(42))
.andThen(x => AsyncEither.fromSync(Either.right(x + 1)))
.fold({
ifRight: x => `Result: ${x}`,
ifLeft: err => `Error: ${err}`
}); // 'Result: 43'

// Using orElse to handle errors in async processing
const handleError = await AsyncEither.fromSync(Either.left('Network error'))
.orElse(err => AsyncEither.fromSync(Either.right(`Recovered from ${err}`)))
.fold({
ifRight: x => `Success: ${x}`,
ifLeft: err => `Failed: ${err}`
}); // 'Success: Recovered from Network error'

// Real-world example with API call
async function fetchUserData(userId: string) {
return AsyncEither.fromPromise(
fetch(`https://api.example.com/users/${userId}`),
error => `Failed to fetch user: ${error.message}`
)
.andThen(response => {
if (!response.ok) {
return AsyncEither.fromSync(Either.left(`HTTP error: ${response.status}`));
}
return AsyncEither.fromPromise(
response.json(),
error => `Failed to parse response: ${error.message}`
);
})
.andThen(user => {
if (!user.id) {
return AsyncEither.fromSync(Either.left('Invalid user data'));
}
return AsyncEither.fromSync(Either.right(user));
})
.orElse(error => {
console.error(`API error: ${error}`);
return AsyncEither.fromSync(Either.left(`Friendly error: Something went wrong`));
});
}
```

##### Running side effects

While not explicitly shown in the provided code, you can use the `fold` method with appropriate handlers to perform side
Expand Down
64 changes: 49 additions & 15 deletions src/either/async-either.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Either, FoldingEither } from './either';
import { Either } from './either';
import { Monad } from '../monad';
import { AsyncRailway, Folding } from '../railway';

/**
* Class representing an asynchronous computation that may result in one of two possible types.
* AsyncEither wraps a Promise that resolves to an Either.
* @template L The type of the left value (usually an error).
* @template R The type of the right value (usually a success).
*/
export class AsyncEither<L, R> implements PromiseLike<Either<L, R>>, Monad<R> {
export class AsyncEither<L, R> implements PromiseLike<Either<L, R>>, Monad<R>, AsyncRailway<R, L> {
private readonly promise: Promise<Either<L, R>>;

private constructor(promise: Promise<Either<L, R>>) {
Expand Down Expand Up @@ -136,25 +137,58 @@ export class AsyncEither<L, R> implements PromiseLike<Either<L, R>>, Monad<R> {
}

/**
* Unwraps the value contained in this AsyncEither instance by applying the appropriate handler for both Left and Right cases.
* Transforms the right value contained in this AsyncEither instance into another AsyncEither instance.
* Implementation of the andThen method from the AsyncRailway interface.
* @template L The type of the left value.
* @template R The type of the right value.
* @template U The type of the new right value.
* @param {(value: R) => AsyncRailway<U, L> | Railway<U, L>} transform The transformation function.
* @returns {AsyncEither<L, U>} A new AsyncEither instance containing the result of the transformation.
*/
andThen<U>(transform: (value: R) => AsyncEither<L, U> | Either<L, U>): AsyncEither<L, U> {
return this.flatMap(transform);
}

/**
* Transforms the left value contained in this AsyncEither instance into another AsyncEither instance.
* @template L The type of the left value.
* @template T The type of the result.
* @param {FoldingEither<R, L, T>} folding The folding object containing the functions to call for each case.
* @returns {Promise<T>} A Promise that resolves to the result of the folding function.
* @example
* const asyncEither = AsyncEither.fromSync(Either.right(5));
* const result = await asyncEither.fold({
* ifRight: value => `Success: ${value}`,
* ifLeft: error => `Error: ${error}`
* });
* console.log(result); // 'Success: 5'
* @template R The type of the right value.
* @template U The type of the new left value.
* @param {(value: L) => AsyncRailway<R, U> | Railway<R, U>} transform The transformation function.
* @returns {AsyncEither<U, R>} A new AsyncEither instance containing the result of the transformation.
*/
async fold<T>(folding: FoldingEither<R, L, T>): Promise<T> {
const either = await this.promise;
orElse<U>(transform: (value: L) => AsyncEither<U, R> | Either<U, R>): AsyncEither<U, R> {
return this.flatMapLeft(transform);
}

/**
* Applies the appropriate function from the folding object based on whether the Either resolves to a Left or Right.
* @template L The type of the left value.
* @template R The type of the right value.
* @template T The return type of the folding functions.
* @param {Folding<'Either', R, L, T>} folding The folding object with functions for handling Left and Right cases.
* @returns {Promise<T>} A promise that resolves to the result of the appropriate folding function.
*/
async fold<T>(folding: Folding<'Either', R, L, T>): Promise<T> {
const either = await this;
return either.fold(folding);
}

/**
* Adds a timeout to this AsyncEither.
* @param {number} ms Timeout in milliseconds.
* @param {() => L} onTimeout Function that returns the error value when timeout occurs.
* @returns {AsyncEither<L, R>} A new AsyncEither with timeout configured.
*/
withTimeout(ms: number, onTimeout: () => L): AsyncEither<L, R> {
return new AsyncEither(
Promise.race([
this.promise,
new Promise<Either<L, R>>((resolve) => setTimeout(() => resolve(Either.left<L, R>(onTimeout())), ms)),
])
);
}

/**
* Creates an AsyncEither from a Promise, handling any rejections with the provided function.
* @template L The type of the left value.
Expand Down
Loading