utilidade

Set of utils tools for Typescript / Javascript.

Usage no npm install needed!

<script type="module">
  import utilidade from 'https://cdn.skypack.dev/utilidade';
</script>

README

utilidade-js

Set of utils Tools for Typescript mainly to enhance Promise API

Dependencies

  • lodash
  • immutable
  • es6-promise

Future

This Future type is meant to be used as Promise, so when you see Future<T>, think "Promise<T> with additionals behaviors having 4 states : NotYetStarted, Pending, Successful, Failed"

Usages Examples

Use Case 1 - Defining Fallback

// Starting by importing Future 
import {Future} from "utilidade";

// Let the following type represent a User.
type User = { name : string }
const getUserFromServer1 : () => User = // Assume this is defined somehow long computation
const getUserFromServer2 : () => User = // Assume this is defined somehow long computation

// So we can do the following : 
Future.from(getUserFromServer1)            // -> Return Future<User> : Create a Future by Trying to get user from Server1.
    .completeBefore({ timeOut : 3000 })    // -> Return Future<User> : This attempt should be completed before 3s get elapsed
    .recover(exception => {})              // -> Return Future<void> : If this attempt fails, we recover from failure ignoring the exception.
    .delay({ duration : 1000 })            // -> Return Future<void> : We will then wait 1s. We still have Future<void>
    .map(ignored => getUserFromServer2())  // -> Return Future<User> : (`map` is almost like `then`) And we will try again to get user from Server2.
    .onComplete({
        ifSuccess : user => console.log("Returned User From Server " + user.name), 
        ifFailure : exception  => console.log("An exception Occured " + exception.toString())
    })

Use Case 2 - Delaying execution and Retrying

You will see why the Future can be in a NotYetStarted state: In fact it is its initial state.

import {LazyFuture} from "utilidade";

const lazyFuture : LazyFuture<User> = 
                Future.lazyFrom(() => getUserFromServer1())
                    .completeBefore({ timeOut : 3000 })
                    .recover(exception => {})
                    .delay({ duration : 1000 })
                    .map(ignored => getUserFromServer2())
// The lazy future previously created is not yet executed 
// We can pass it around as parameters

// We can also execute it 
lazyFuture.start(); // Return Future<User>
lazyFuture.start(); // Won't retry as it already have been evaluated

// We can also execute it after 3seconds
lazyFuture.startAfter({ duration : 3000 }) // Return Future<User>

// We can also retry all of previously defined computations
lazyFuture.reinitialize(); // Return a new LazyFuture<User> Reset to its initial State
lazyFuture.reinitialize().start()   // Return a new LazyFuture<User> Reset to 
                                    // its initial State start it and Return Future<User>


Use Case 3 - Rendering Promise inside React component

There exists in Future class a method called fold that will "open and see in" the future and following the current state of the future, a computation will append with the value inside the future object if any

// Here is the render we can write with the fold method 
render() {
    const getUserFromServerFuture : Future<User> = ... // Assume we have a future fetching the user from Server
    return (
        <div>
            {
                getUserFromServerFuture.fold({ // fold method will "open and see in" the future
                    ifSuccess : user => <span>Hello {user.name}</span>,
                    ifFailure : exception => <span>An Error Occured</span>,
                    otherwise : () => <Loading />
                })
            }
        </div>
    ); 
} 

4 - Comparison with Promise API

Promise<T> Future<T>
Query Internal State
None future.isCompleted()
future.isNotYetStarted()
future.isPending()
future.isSuccess()
future.isFailure()
Pull the value out
None future.valueOrNull()
future.exceptionOrNull()
Transform
then<U>(fn : T => U) : Promise<U>
then<U>(fn : T => Promise<U>):Promise<U>

catch<U>(fn: any => U):Promise<U>
catch<U>(fn: any => Promise<U>):Promise<U>

finally<U>(fn: () => U):Promise<U>
finally<U>(fn: () => Promise<U>):Promise<U>
map<U>(fn : T => U) : Future<U>
flatMap<U>(fn : T => Future<U>): Future<U>

recover<U>(fn : T => U) : Future<U>
flatRecover<U>(fn : T => Future<U>) : Future<U>

transform<U>(fn: () => U): Future<U>
flatTransform<U>(fn: () => Future<U>): Future<U>
Timeout / Delay
None delay(obj : { duration: number }): Future<T>
completeBefore(obj:{ timeOut: number }):Future<T>
Callback
None (but can be simulated with then/catch/finally) onComplete(fn : () => void): void
onSuccess(fn : T => void) : void
onFailure(fn: Exception => void): void
Static Method Creation
Promise.resolve<T>(value:T) : Promise<T>
Promise.reject<T>(exception:any):Promise<T>


Future.successful<T>(value:T): Future<T>
Future.failed<T>(exception:Exception): Future<T>
Future.foreverPending<T>() : Future<T>
Future.notYetStarted<T>(): Future<T>
Static Method Helpers
Promise.all<T>(promises:[Promise<T>]):Promise<[T]>
Promise.race<T>(promises:[Promise<T>]): Promise<T>





Future.all<T>(futures: [Future<T>]): Future<[T]>
Future.firstCompletedOf<T>(futures: [Future<T>]): Future<T>
Future.lastCompletedOf<T>(futures:[Future<T>]):Future<T>
Future.startAfter<T>(timeout:number,
                                    fn: () => Future<T>):Future<T>
Future.executeAfter<T>(timeout:number,
                                       fn: () => T):Future<T>

Nomenclature


Why not using catch/then/finally ?

Because catch is a reserved keyword, we choose NOT to try to force the definition of a method named catch. Hence we did not choose then because we knew that catch won't be available. If a then method were defined, it would have been weird if the catch method were missing. The same goes for finally reserved keyword.


Why naming map instead of then ?

What does the map allows you to do ? Create a new data structure by opening you current data structure and apply a function on every items inside of it.

Array(1, 2, 3).map(i => i + 1) // Create a new Array starting from the current Array and applying a function to every elements inside of it 
Future(1).map(i => i + 1)      // Create a new Future starting from the current Future and applying a function to every elements inside of it 

NB : See this link for some theorical resource about the map function.

Why naming flatMap / flatRecover / flatTransform ?

We all know the mapfunction defined in array

const array1 = Array(1, 2, 3).map(i => i * 2)   // Return Array(2, 4, 6) 
const array2 = Array(1, 2, 3).map(i => { num : i })   // Return Array({num : 1}, {num : 2}, {num : 3}) 

Now Assume we have the flatMap method defined on Array by this behaviour :

const array3 = Array(1, 2, 3).flatMap(i => Array(i, i))   // Return Array(1, 1, 2, 2, 3, 3)

If We use the map function defined in Array, with the same argument as flatMap function previously defined , ie i => Array(i, i), we will have the following result

const array4 = Array(1, 2, 3).map(i => Array(i, i))   // Return Array(Array(1, 1), Array(2, 2), Array(3, 3)) 

Now assume we have the flatten method defined on Array by this behaviour :

const array5 = Array(Array(1)).flatten             // Return Array(1)
const array6 = Array(Array(1), Array(2)).flatten   // Return Array(1, 2)

So to get what we get in array1, we have to "flatten" the array2

// array3 == array4.flatten 
// because array4 = Array(1, 2, 3).map(i => Array(i, i)) 
// array3F == Array(1, 2, 3).map(i => Array(i, i)).flatten 

We then have the nomenclature FlatMap means map and then flatten The same pattern goes for Set, List, and Future and we could define flatMap such as

// Set(1).flatMap(i => Set(i + 1))          // Return Set(2)
// List(1).flatMap(i => List(i + 1))        // Return List(2)
// Future(1).flatMap(i => Future(i + 1))    // Return Future(2)
// and so one....

NB : With this flatMap operations and others functions having some other properties, the Future Data structure is a Monad