Hooks
Synchronous hooks
The Hooks
class provides a convenient way to change and inspect arguments and return value of synchronous functions.
Creating middlewares
In order to use the Hooks
class we need to create middlewares
:
// file: middlewares.ts
import type { MiddlewareFn } from "@daiso-tech/core/utilities";
// Creating function that returns a middleware that will log the arguments and return value.
export function log<TParameters extends unknown[], TReturn>(): MiddlewareFn<
TParameters,
TReturn,
{ funcName: string }
> {
return (args, next, { name: funcName }) => {
console.log("FUNCTION_NAME:", funcName);
console.log("ARGUMENTS:", args);
const value = next(...args);
console.log("RETURN:", value);
return value;
};
}
// Creating function that returns a middleware that will log the execution time.
export function time<TParameters extends unknown[], TReturn>(): MiddlewareFn<
TParameters,
TReturn
> {
return (args, next) => {
const start = performance.now();
const value = next(...args);
const end = performance.now();
const time = end - start;
console.log("TIME:", `${String(time)}ms`);
return value;
};
}
Note each middleware receives the passed arguments, the next function to execute and the function name. The middleware must then return a value.
Using middlewares
Now we need to apply the middlewares to a function:
// file: main.ts
import { Hooks } from "@daiso-tech/core/utilities";
import { log, time } from "./middlewares.js";
function add(a: number, b: number): number {
return a + b;
}
// Applying the middleware on the add function
const enhancedAdd = new Hooks(add, [log(), time()]);
// Will log the function name, arguments, return value and execution time.
const result = enhancedAdd.invoke(1, 2);
// Will be 3.
console.log(result);
Note middlewares apply left to right: each wraps the next, with the leftmost being the outermost layer and the rightmost wrapping the original function.
Note the middlewares are reusable and can be applied to other functions:
// file: main.ts
import { Hooks } from "@daiso-tech/core/utilities";
import { log, time } from "./middlewares.js";
function average(...nbrs: number[]): number {
let average = 0;
for (const nbr of nbrs) {
average += nbr;
}
return average / nbr.length;
}
const enhancedAverage = new Hooks(average, [log(), time()]);
Setting function name
If you use an anonymous function, you must set its name manually; otherwise, it will default to 'func'.
import { Hook } from "@daiso-tech/core/utilities";
const enhancedAdd = new Hooks(
(a: number, b: number): number => a + b,
[
(args, next, { name: funcName }) => {
console.log(funcName);
return next(...args);
},
],
{
name: "add",
},
);
Providing additional context
The middleware supports custom context, allowing you to pass any relevant information.
import { Hook, type MiddlewareFn } from "@daiso-tech/core/utilities";
function add(a: number, b: number): number {
return a + b;
}
type MiddlewareContext = {
a: number;
b: number;
c: number;
};
function createMiddleware<
TParameters extends unknown[],
TReturn,
>(): MiddlewareFn<TParameters, TReturn, MiddlewareContext> {
return (args, next, { context }) => {
// Will print { a: 1, b: 2, c: 3 }
console.log(context);
return next(...args);
};
}
const enhancedAdd = new Hooks(add, [createMiddleware()], {
// You provide context here
context: {
a: 1,
b: 2,
c: 3,
},
});
Converting to a function
You can convert Hooks
to a reguler function.
import { Hook } from "@daiso-tech/core/utilities";
// add is now a function and not Hook instance
const add = new Hooks(
(a: number, b: number): number => a + b,
[
(args, next) => {
console.log("Middleware applied");
return next(...args);
},
],
{
name: "add",
},
).toFunc();
Deriving Hook instances
You can derive a new Hooks
instance from another instance.
import { Hook } from "@daiso-tech/core/utilities";
const addA = new Hooks(
(a: number, b: number): number => a + b,
[
(args, next) => {
console.log("Middleware A applied");
return next(...args);
},
],
{
name: "add",
},
);
// Will include middleware A and B
const addB = addA.pipe((args, next) => {
console.log("Middleware B applied");
return next(...args);
});
// Will only apply the middleware if the statement is true
// If the statement is true then middleware A, B and C will be included
// If the statement is false then only middleware A and B will be included
const addC = addA.pipeWhen(false, (args, next) => {
console.log("Middleware C applied");
return next(...args);
});
Note that the Hooks
is immutable, meaning any configuration override returns a new instance rather than modifying the existing one.
Asynchronous hooks
The AsyncHooks
class is similar to Hooks, but it works with both asynchronous and synchronous functions. Unlike Hooks
, this class always returns a Promise.
Usage
import { AsyncHooks } from "@daiso-tech/core/utilities";
// Works with synchronous function
const add1 = new AsyncHooks(
(a: number, b: number): number => a + b,
[
async (args, next) => {
console.log("Middleware applied");
return await next(...args);
},
],
{
name: "add",
},
);
// Works with asynchronous function
const add2 = new AsyncHooks(
async (a: number, b: number): Promise<number> => a + b,
[
async (args, next) => {
console.log("Middleware applied");
return await next(...args);
},
],
{
name: "add",
},
);
Binding AbortSignal
By binding an AbortSignal
via AbortSignalBinder
, both the middleware and the function gain mutual cancellation control, provided the function handles AbortSignal
.
import { AsyncHooks, type AsyncMiddlewareFn } from "@daiso-tech/core/utilities";
async function fetchData(url: string, signal?: AbortSignal): Promise<unknown> {
const response = await fetch(url, { signal });
return await response.json();
}
const fetchDataSignalBinder: AbortSignalBinder<Parameters<typeof fetchData>> = {
// Return the signal that is passed to arguments or null
getSignal: (args) => args[1],
// Replace the argument where the signal is passed
// with a signal that will be aborted from the middleware or the function
forwardSignal: (args, signal) => {
args[1] = signal;
},
};
function timeout<TParameters extends unknown[], TReturn>(
repetition: number,
): AsyncMiddlewareFn<TParameters, TReturn> {
return async (args, next, { abort, signal }) => {
// We check if the function is already aborted
// then will throw early
if (signal.aborted) {
throw signal.reason;
}
// We abort the function when it execdes 2 seconds.
const id = setTimeout(() => abort("Timed out"), 2000);
const clear = () => clearTimeout(id);
try {
// We clear the timeout if function is aborted before it execdes 2 seconds.
signal.addEventListener("abort", clear, {
once: true,
});
return await next(...args);
} finally {
// We de cleanup work
// Remove the listener
signal.removeEventListener("abort", clear);
// Clear the timeout
clear();
}
};
}
const fetchDataEnhanced = new AsyncHooks(fetchData, [timeout()], {
signalBinder: fetchDataSignalBinder,
});