1.0.0 • Published 4 months ago

callback-utility v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

Callback utility

An utility handler to deal with callback functions and avoid callback "hell".

You can run several functions in parallel or in sequence (even mixing both types) and receive a single result object with results for every call.

In sequential statements, you can access results from immediately previous function, creating cascading calls (waterfall).

You can get the result in a Promise (using async/await) or providing a single callback function that receives the Result object with results for every call.

Example

Creating a log file from the content of several files using node:fs (callbacks). The order in which every file is appended to log is not important, so we can parallelize it.

The code will:

  • delete current log file (if exists) with fs.rm()
  • execute (in parallel) for every file
    • read content with fs.readFile(), then (sequentially)
    • write content retrieved from previous function to log file with fs.appendFile()

!NOTE Code excerpts will be provided in TypeScript. To use it in plain JavaScript, just ignore all types declaration (the : type part of the code).

/**
 * Creates a log file from several files
 */

import { CB } from "callback-utility";


const logFile: string = path.resolve(__dirname, "mainLog.log"),
      file1:   string = path.resolve(__dirname, "file1.log"),
      file2:   string = path.resolve(__dirname, "file2.log"),
      file3:   string = path.resolve(__dirname, "file3.log"),
      file4:   string = path.resolve(__dirname, "file4.log");


// Create execution structure 
const structCB = 
    CB.s ( // šŸ „ sequential structure as root

        // Delete current log file
        CB.f ( fs.rm, logFile, {force: true}), // šŸ „ Creates a function structure using CB.f()


        // Create log from several files
        CB.p ( // šŸ „ parallel structure, since the order in which every file is written in
               //    log is not important (can be parallelized)

            CB.s ( // šŸ „ sequential structure
                CB.f ( fs.readFile, file1, {encoding: 'utf-8'} ),      // šŸ „ read content 
                CB.f ( fs.appendFile, strLogFile, CB.PREVIOUS_RESULT1) // šŸ „ write results from 
                                                                       //    previous call to log file
            ),

            // The same (in parallel) for every file ...
            CB.s (
                CB.f ( fs.readFile, file2, {encoding: 'utf-8'} ),
                CB.f ( fs.appendFile, logFile, CB.PREVIOUS_RESULT1)
            ),
            CB.s (
                CB.f ( fs.readFile, file3, {encoding: 'utf-8'} ),
                CB.f ( fs.appendFile, logFile, CB.PREVIOUS_RESULT1)
            ),
            CB.s (
                CB.f ( fs.readFile, file4, {encoding: 'utf-8'} ),
                CB.f ( fs.appendFile, logFile, CB.PREVIOUS_RESULT1)
            )
        )
    );


// Execute and retrieve results using Promise (async/await)
const objResult = await CB.e (structCB);


// Check results
if (objResult.timeout || objResult.error)
    console.log("Something went wrong while creating the log");
else
    console.log("Log created");

Installation and usage

To install, run this command in your terminal:

npm install callback-utility

Load it in your code as ECMAScript (esm) or CommonJS (cjs) module.

// esm
import { CB } from "callback-utility";
// cjs
const { CB } = require("callback-utility");

!TIP It can be used in JavaScript or TypeScript codes (no need for additional types).

The execution structure

The execution structure stores information about what functions to run (including arguments) and when (execution order).

It is composed of three different structures:

Function structure (FunctionStruct)

Stores info about what function to execute and the arguments to be used, except for the callback (which is always the last one).

It is created through CB.f() function, which has two overloaded signatures:

// Without alias
CB.f ( fn: Function,    // šŸ „ function to be executed
       ...args: any[]); // šŸ „ arguments to be passed to function
// With alias
CB.f ( alias: string,   // šŸ „ alias for this call, to facilitate results retrieval
       fn: Function,    // šŸ „ function to be executed
       ...args: any[]); // šŸ „ arguments to be passed to function

Example using fs.writeFile() to write some text in UTF-8 enconding to a file:

// Mind:
// - don't include parenthesis after function name
// - don't include the callback parameter
CB.f (fs.writeFile, PathToFile, TextToWrite, "utf-8")

Parallel structure (ParallelStruct)

Stores info about sub structures to be executed in parallel. Every sub structure can be:

  • a Function Structure (FunctionStruct),
  • a Sequential Structure (SequentialStruct),
  • or even another Parallel Structure (ParallelStruct).

It is created through CB.p() function, which has two overloaded signatures:

// Without alias
CB.p ( ...subStructs: FunctionStruct | ParallelStruct | SequentialStruct);
// With alias
CB.p ( alias: string,
      ...subStructs: FunctionStruct | ParallelStruct | SequentialStruct);

Example using fs.writeFile() to write text in UTF-8 enconding to 3 files in parallel:

CB. p (
    CB.f (fs.writeFile, PathToFile1, TextToWrite1, "utf-8"),
    CB.f (fs.writeFile, PathToFile2, TextToWrite2, "utf-8"),
    CB.f (fs.writeFile, PathToFile3, TextToWrite3, "utf-8")
);

Sequential structure (SequentialStruct)

Stores info about sub structures to be executed in sequence (execution only starts after the previous one finishes). Every sub structure can be:

  • a Function Structure (FunctionStruct),
  • a Parallel Structure (ParallelStruct),
  • or even another Sequential Structure (SequentialStruct).

It is created through CB.s() function, which has two overloaded signatures:

// Without alias
CB.s ( ...subStructs: FunctionStruct | ParallelStruct | SequentialStruct)
// With alias
CB.s ( alias: string,
      ...subStructs: FunctionStruct | ParallelStruct | SequentialStruct)

Results from the immediately previous call can be used as arguments in a Function Structure

Example using fs.readFile() and fs.appendFile() to read text from a file and then append it to another file:

CB.s (
    CB.f ( fs.readFile, PathToFileFrom, {encoding: 'utf-8'} ),
    CB.f ( fs.appendFile, PathToFileTo, CB.PREVIOUS_RESULT1)
)

Accessing previous results

To use previous results, pass one of the following tokens as arguments to your function:

TokenDescription
CB.PREVIOUS_ERRORValue of the first argument (which is the error) passed to callback function
CB.PREVIOUS_RESULT1Value of the first argument after the error (i.e. the second argument) passed to callback function
CB.PREVIOUS_RESULT2Value of the second argument after the error passed to callback function
CB.PREVIOUS_RESULT3Value of the third argument after the error passed to callback function
CB.PREVIOUS_RESULT4Value of the fourth argument after the error passed to callback function
CB.PREVIOUS_RESULT5Value of the fifth argument after the error passed to callback function
CB.PREVIOUS_RESULT6Value of the sixth argument after the error passed to callback function
CB.PREVIOUS_RESULT7Value of the seventh argument after the error passed to callback function
CB.PREVIOUS_RESULT8Value of the eighth argument after the error passed to callback function
CB.PREVIOUS_RESULT9Value of the ninth argument after the error passed to callback function

!WARNING If you try to use a token in the very first function of a sequential structure, an exception will be thrown, since there is no previous result.

!WARNING If you try to use a token in a parallel structure, an exception will be thrown.

Anatomy of execution structure

Execution structure is a tree where:

  • all leaves are Function Structures,
  • all nodes are Parallel Structures or Sequential Structures,
  • root is a Parallel Structure or Sequential Structure.

An example:

Parallel            šŸ „ root
┣━ Function         šŸ „ leaf
┣━ Sequential       šŸ „ node
ā”ƒ  ┣━ Function      šŸ „ leaf
ā”ƒ  ┣━ Function      šŸ „ leaf
ā”ƒ  ┗━ Parallel      šŸ „ node
ā”ƒ     ┣━ Function   šŸ „ leaf
ā”ƒ     ┗━ Function   šŸ „ leaf 
┗━ Paralell         šŸ „ node
   ┣━ Function      šŸ „ leaf
   ┗━ Function      šŸ „ leaf

Executing functions

Use the function CB.e() to execute a previously created execution structure and get the results.

You can do that using async/await (Promise) or providing a callback function.

Callback function

To use the callback approach, provide a function as last argument to execution function.

CB.e (execStruct, callback);

The callback function must have the signature:

function (error:   boolean |,   // šŸ „ true, if an error was returned from any function,
                   CBException  //    or CBException, if any exception was thrown during execution
          timeout: boolean,     // šŸ „ true if ellapsed execution time exceeds defined timeout
          result:  Result);     // šŸ „ Result object

Async/await

To use async/await approach, just ignore the callback argument of execution function

const result: Result = await CB.e (execStruct);

Anatomy of execution function (CB.e())

The execution function has several overloads

    // For async/await approach
    function e (execStruct:    ParallelStruct | SequentialStruct): Promise<Result>;
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                timeout:       number): Promise<Result>;
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                timeout:       number,
                breakOnError:  boolean): Promise<Result>;
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                timeout:       number,
                breakOnError:  boolean,
                stats:         boolean): Promise<Result>;


    // For callback approach
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                callback:      TCallback): void;
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                timeout:       number,
                callback:      TCallback): void;
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                timeout:       number,
                breakOnError:  boolean,
                callback:      TCallback): void;
    function e (execStruct:    ParallelStruct | SequentialStruct, 
                timeout:       number,
                breakOnError:  boolean,
                stats:         boolean,
                callback:      TCallback): void
ArgumentDescriptionDefault value
execStructExecution structure (ParallelStruct or SequentialStruct) to be executed
timeoutMaximum time (in milliseconds) for the execution to complete5000
breakOnErrorDefines if execution must be stopped at first error returned from a function structuretrue
statsDefines if the execution time ellapsed must be gatheredfalse
callbackCallback function to retrieve results (only for callback approach)

Examples:

// Using await/async
const result: Result = await CB.e (executionStructure); // šŸ „ Execute with default values:
                                                        //        timeout      = 5000
                                                        //        breakOnError = true
                                                        //        stats        = false

const result: Result = await CB.e (executionStructure,  // šŸ „ Execution structure:
                                   2000,                // šŸ „ 2 seconds for timeout
                                   false,               // šŸ „ Don't stop execution if error is returned
                                   true);               // šŸ „ Gather stats info
// Using callback
CB.e (executionStructure,           // šŸ „ Execution structure
      (error, timeout, result) =>   // šŸ „ Callback function
      {
          if (error || timeout)
              console.log("Something wrong");
          else
              // do stuff ...

      });

CB.e (executionStructure,           // šŸ „ Execution structure
      3500,                         // šŸ „ 3.5 seconds for timeout
      true,                         // šŸ „ Stop execution if any error is returned
      true,                         // šŸ „ Gather stats info
      (error, timeout, result) =>   // šŸ „ Callback function
      {
          if (error || timeout)
              console.log("Something wrong");
          else
              // do stuff ...

      });

Getting results

Getting results by position

/**
 * Reading content from several files in parallel
 */

const struct = CB.p ( 
                   CB.f (fs.readFile, PathToFile1, {encoding: 'utf-8'}), // šŸ „ position: 1
                   CB.f (fs.readFile, PathToFile2, {encoding: 'utf-8'}), // šŸ „ position: 2
                   CB.f (fs.readFile, PathToFile3, {encoding: 'utf-8'}), // šŸ „ position: 3
                   CB.f (fs.readFile, PathToFile4, {encoding: 'utf-8'})  // šŸ „ position: 4
               );

const result = await CB.e (struct);


if (result.error || result.timeout)
{
    console.log("Something wrong");
}
else
{
    //                          ⮦ result position
    const file1Content = result[1].results[0];
    const file1Content = result[2].results[0];
    const file1Content = result[3].results[0];
    const file1Content = result[4].results[0];
    //                                     ⮤ first result for every function, i.e., the first 
    //                                       argument passed to callback
}

Getting results by alias

/**
 * Reading content from several files in parallel
 */

const struct = CB.p ( 
                   //       ⮦ aliases
                   CB.f ("file1", fs.readFile, PathToFile1, {encoding: 'utf-8'}),
                   CB.f ("file2", fs.readFile, PathToFile2, {encoding: 'utf-8'}),
                   CB.f ("file3", fs.readFile, PathToFile3, {encoding: 'utf-8'}),
                   CB.f ("file4", fs.readFile, PathToFile4, {encoding: 'utf-8'})
               );

const result = await CB.e (struct);


if (result.error || result.timeout)
{
    console.log("Something wrong");
}
else
{
    //                                        ⮦ aliases
    const file1Content = result.getByAlias("file1").results[0];
    const file1Content = result.getByAlias("file2").results[0];
    const file1Content = result.getByAlias("file3").results[0];
    const file1Content = result.getByAlias("file4").results[0];
}

!WARNING Aliases are case-sensitive

!WARNING If you use the same alias more than once, an exception will be thrown

Anatomy of Results

Result object (Result)

Results for every Execution structure are stored in an instance of the Result class, which is an array-like object, i.e.:

  • it has a lenght property,
  • it can be iterated using a for statement,
  • results can be retrieved by position

Results are stored in the same position as they were coded:

  • Results for FunctionStruct are stored in a FunctionResult object
  • Results for ParallelStruct are stored in a ParallelResult object
  • Results for SequentialStruct are stored in a SequentialResult object

Example:

Parallel            šŸ „ result[0]  : ParallelResult
┣━ Function         šŸ „ result[1]  : FunctionResult
┣━ Sequential       šŸ „ result[2]  : SequentialResult
ā”ƒ  ┣━ Function      šŸ „ result[3]  : FunctionResult 
ā”ƒ  ┣━ Function      šŸ „ result[4]           ⇣
ā”ƒ  ┗━ Parallel      šŸ „ result[5]
ā”ƒ     ┣━ Function   šŸ „ result[6]
ā”ƒ     ┗━ Function   šŸ „ result[7]
┗━ Paralell         šŸ „ result[8]
   ┣━ Function      šŸ „ result[9]
   ┗━ Function      šŸ „ result[10]

Properties

error
Boolean indicating if any error was returned by a function or if any exception was thrown during execution.

length
The number of results stored (structures executed). It is the same as the quantity of CB.f(), CB.p() or CB.s() used to create the execution structure.

stats
Milliseconds ellapsed during execution.

!WARNING Stats will be gathered only if the value of stats argument of CB.e() was set to true

Methods

getByAlias( alias: string)
Get the result for the provided alias.

getErrors()
Get an array with all errors returned from function executions.

Errors returned by functions will be wrapped in a CBException object. You can get the function that originated the error checking the details property of the exception, which will provide position (callIndex) and alias (callAlias, if provided) for the faulty structure:

...
const errors: CBException[] = result.getErrors();

for (let error of errors)
{
    console.log(error.details.callIndex); // Position of the function in execution structure and in result object
    console.log(error.details.callAlias); // Execution structure alias, if provided
}

Function results (FunctionResult)

FunctionResult stores results from FunctionStruct execution.

Properties

error
Stores the first argument passed to callback function. By convention, the first argument of a callback function indicates any error that may have occured during execution.

results
Stores, in an array, all arguments passed to callback function, except the first one.

Example: getting results from fs.read()

// Signature for fs.read() callback is as follows:
function(err:       Error,   // šŸ „ will be stored in FunctionResult.error
         bytesRead: number,  // šŸ „ will be stored in FunctionResult.results[0]
         buffer     Buffer); // šŸ „ will be stored in FunctionResult.results[1]
const struct = CB.s (
                   CB.f (fs.read, fd, buffer, offset, length, position),
                   ...
               );
const result: Result = await CB.e (struct);

if (!result.error && !result.timeout) // šŸ „ no error, go on...
{
    const bytesRead: number = result[1].results[0];
    const buffer:    Buffer = result[1].results[1];
}

stats
Milliseconds ellapsed during execution.

!WARNING Stats will be gathered only if the value of stats argument of CB.e() was set to true

Parallel and sequential results (ParallelResult, SequentialResult)

ParallelResult and SequentialResultResults stores results for every sub-structure executed. It is an array-like object, i.e.:

  • it has a lenght property,
  • it can be iterated using a for statement,
  • results can be retrieved by position

It is pretty similiar to FunctionResult class, but error and results properties return arrays with the same hierarchy of the sub structures executed.

!TIP Retrieving results through ParallelResult or SequentialResult can be tricky, specially for complex structures (too many nodes). It is preferable to deal with each child FunctionResult instead.

Properties

error
Array with all errors returned from sub structures execution. The array will keep the same "hierarchy" of the original execution structure, i.e., there will be array inside arrays for child structures.

length
The number of results stored (sub structures executed).

results
An array with all results from all sub structures executed. The array will keep the same "hierarchy" of the original execution structure, i.e., there will be array inside arrays for child structures.

stats
Milliseconds ellapsed during execution.

!WARNING Stats will be gathered only if the value of stats argument of CB.e() was set to true

Example:

For an execution structure like:

Parallel
┣━ Function
┣━ Sequential        šŸ „ result[2]
ā”ƒ  ┣━ Function         šŸ „ child struct 1
ā”ƒ  ┣━ Function         šŸ „ child struct 2
ā”ƒ  ┗━ Parallel         šŸ „ child struct 3
ā”ƒ     ┣━ Function         šŸ „ grand child struct 1
ā”ƒ     ┗━ Function         šŸ „ grand child struct 2
┗━ Paralell
   ┣━ Function
   ┗━ Function

The result[2] will give us the following results:

result[2].error = [
                     error,    // šŸ „ child 1 error
                     error,    // šŸ „ child 2 error
                     [         // šŸ „ child 3 error (an array, since it is a parallel struct)
                        error, //    šŸ „ grand child 1 error
                        error  //    šŸ „ grand child 2 error
                     ]
                  ];

result[2].results = [
                        result[],    // šŸ „ child 1 result (an array)
                        result[],    // šŸ „ child 2 result
                        [            // šŸ „ child 3 result (an array of arrays, since it is a parallel struct)
                           result[], //    šŸ „ grand child 1 result
                           result[]  //    šŸ „ grand child 2 result
                        ]
                     ];

Checking errors

// Using async/await

// ...
const result: Result = await CB.e (struct);

if (result.error && result.timeout) 
    console.log("Something wrong");
else
    // Do stuff ...
// Using callback

// ...
CB.e (executionStructure,
      (error, timeout, result) =>
      {
          if (error || timeout)
              console.log("Something wrong");
          else
              // do stuff ...

      });

Exceptions

Thrown exceptions and values from Result.getErrors() will be instancess of CBException class, which extends Error and add the properties:

baseException
The original emitted exception that is been wrapped in CBException

errorNumber
A unique number for every type of exception mapped

details
This will be set when the exception comes from a function execution or when a function execution returns an error in callback. It will have a callIndex value and may have a callAlias value (if provided).

  • details.callIndex: number of the indexed position of the function structure in Result.
  • details.callAlias: Alias (if provided) for the function structure that emitted the error.

explanation
A brief text with clues as to what might have gone wrong

Feedback

If you have any comment, sugestion or if you find any problem using callback-utility, create, please, an issue in GitHub projec's page.

I do appreciate any feedback and will do my best to answer quickly.