callback-utility v1.0.0
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()
- read content with
!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:
Token | Description |
---|---|
CB.PREVIOUS_ERROR | Value of the first argument (which is the error) passed to callback function |
CB.PREVIOUS_RESULT1 | Value of the first argument after the error (i.e. the second argument) passed to callback function |
CB.PREVIOUS_RESULT2 | Value of the second argument after the error passed to callback function |
CB.PREVIOUS_RESULT3 | Value of the third argument after the error passed to callback function |
CB.PREVIOUS_RESULT4 | Value of the fourth argument after the error passed to callback function |
CB.PREVIOUS_RESULT5 | Value of the fifth argument after the error passed to callback function |
CB.PREVIOUS_RESULT6 | Value of the sixth argument after the error passed to callback function |
CB.PREVIOUS_RESULT7 | Value of the seventh argument after the error passed to callback function |
CB.PREVIOUS_RESULT8 | Value of the eighth argument after the error passed to callback function |
CB.PREVIOUS_RESULT9 | Value 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
Argument | Description | Default value |
---|---|---|
execStruct | Execution structure (ParallelStruct or SequentialStruct ) to be executed | |
timeout | Maximum time (in milliseconds) for the execution to complete | 5000 |
breakOnError | Defines if execution must be stopped at first error returned from a function structure | true |
stats | Defines if the execution time ellapsed must be gathered | false |
callback | Callback 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 aFunctionResult
object - Results for
ParallelStruct
are stored in aParallelResult
object - Results for
SequentialStruct
are stored in aSequentialResult
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 ofCB.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 ofCB.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
orSequentialResult
can be tricky, specially for complex structures (too many nodes). It is preferable to deal with each childFunctionResult
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 ofCB.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 inResult
.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.
4 months ago