Resilience middlewares
Fallback
The fallback
middleware adds fallback value when an error occurs:
Usage
import { fallback } from "@daiso-tech/core/async";
import { AsyncHooks } from "@daiso-tech/core/utilities";
function unstableFn(): number {
// We simulate a function that can throw unexpected errors
if (Math.round(Math.random() * 1.5) === 0) {
throw new Error("Unexpected error occured");
}
return Math.round((Math.random() + 1) * 99);
}
const fn = new AsyncHooks(unstableFn, [
fallback({
fallbackValue: 1,
}),
]);
// Will never throw and when error occurs the fallback value will be returned.
console.log(await fn.invoke());
You can provide LazyPromise
, synchronous and asynchronous Invokable
as fallback value.
Custom ErrorPolicy
You can define an ErrorPolicy
to specify fallback values for specific error cases:
const fn = new AsyncHooks(unstableFn, [
fallback({
fallbackValue: 1,
// Will only fallback errors that are not a TypeError
errorPolicy: (error) => !(error instanceof TypeError),
}),
]);
Callbacks
You can add callback Invokable
that will be called before the fallback value is returned.
const fn = new AsyncHooks(unstableFn, [
fallback({
fallbackValue: 1,
onFallback: (fallbackData) => console.log(fallbackData),
}),
]);
For more details about onFallback
callback data, see the OnFallbackData type.
Hedging
The hedging middlewares allow you to send request to multiple redundant services and use the first successful response. The library offers one strategy:
-
The
sequentialHedging
middleware executes the primary function followed by fallbackInvokable:s
in sequence. It:-
Returns immediately with the first successful result.
-
Throws an error only if all
Invokable:s
fail.
-
Usage
import { sequentialHedging } from "@daiso-tech/core/async";
import { AsyncHooks } from "@daiso-tech/core/utilities";
async function fetchData(url: string, signal?: AbortSignal): Promise<Response> {
return await fetch(url, {
signal,
});
}
async function fetchDataFallback1(
url: string,
signal?: AbortSignal,
): Promise<Response> {
return await fetch(`ENDPOINT_1/${url}`, {
signal,
});
}
async function fetchDataFallback2(
url: string,
signal?: AbortSignal,
): Promise<Response> {
return await fetch(`ENDPOINT_2/${url}`, {
signal,
});
}
async function fetchDataFallback3(
url: string,
signal?: AbortSignal,
): Promise<Response> {
return await fetch(`ENDPOINT_3/${url}`, {
signal,
});
}
const fetchDataEnhanced = new AsyncHooks(fetchData, [
sequentialHedging({
fallbacks: [fetchDataFallback1, fetchDataFallback2, fetchDataFallback3],
}),
]);
const response = await fetchDataEnhanced.invoke("ENDPOINT");
console.log(await response.json());
Note when abortion occurs, it will not abort the hedging middleware fallbacks executions. To ensure correct abortion behavior, provide an AbortSignalBinder
to AsyncHooks
.
For further information about AbortSignalBinder
and AsyncHooks
refer to Hooks documentation.
const fetchDataEnhanced = new AsyncHooks(
fetchData,
[
sequentialHedging({
fallbacks: [
fetchDataFallback1,
fetchDataFallback2,
fetchDataFallback3,
],
}),
],
{
signalBinder: {
getSignal: (args) => args[1],
forwardSignal: (args, signal) => {
args[1] = signal;
},
},
},
);
const response = await fetchDataEnhanced.invoke("ENDPOINT");
console.log(await response.json());
Providing timeout and retry
You can abort the hedging calls after a specified time period:
import { TimeSpan } from "@daiso-tech/core/utilities";
import { retry, timeout } from "@daiso-tech/core/async";
const fetchDataEnhanced = new AsyncHooks(
fetchData,
[
sequentialHedging({
fallbacks: [
fetchDataFallback1,
fetchDataFallback2,
fetchDataFallback3,
],
// You can add middlewares that will apply to both the primary function and all fallback functions.
middlewares: [
timeout({
waitTime: TimeSpan.fromSeconds(2),
}),
retry({
maxAttempts: 4,
}),
],
}),
],
{
signalBinder: {
getSignal: (args) => args[1],
forwardSignal: (args, signal) => {
args[1] = signal;
},
},
},
);
Named fallbacks
You can provide named fallbacks which is useful when debugging:
const fetchDataEnhanced = new AsyncHooks(
fetchData,
[
sequentialHedging({
fallbacks: [
{
name: "fallback-a",
invokable: fetchDataFallback1,
},
{
name: "fallback-b",
invokable: fetchDataFallback2,
},
{
name: "fallback-c",
invokable: fetchDataFallback3,
},
],
}),
],
{
signalBinder: {
getSignal: (args) => args[1],
forwardSignal: (args, signal) => {
args[1] = signal;
},
},
},
);
Callbacks
You can add callback Invokable
that will be called before execution attempt:
const fetchDataEnhanced = new AsyncHooks(
fetchData,
[
sequentialHedging({
fallbacks: [
fetchDataFallback1,
fetchDataFallback2,
fetchDataFallback3,
],
onHedgeAttempt: (data) => console.log(data),
}),
],
{
signalBinder: {
getSignal: (args) => args[1],
forwardSignal: (args, signal) => {
args[1] = signal;
},
},
},
);
For more details about onHedgingAttempt
callback data, see the OnHedgeAttemptData type.
You can add callback Invokable
that will be called when a error occurs:
const fetchDataEnhanced = new AsyncHooks(
fetchData,
[
sequentialHedging({
fallbacks: [
fetchDataFallback1,
fetchDataFallback2,
fetchDataFallback3,
],
onHedgeError: (data) => console.log(data),
}),
],
{
signalBinder: {
getSignal: (args) => args[1],
forwardSignal: (args, signal) => {
args[1] = signal;
},
},
},
);
For more details about OnHedgeError
callback data, see the OnHedgeErrorData type.
Retry
The retry
middleware enables automatic retries for all errors or specific errors, with configurable backoff policies. An error will be thrown when all retry attempts fail.
Usage
import { retry } from "@daiso-tech/core/async";
import { AsyncHooks } from "@daiso-tech/core/utilities";
function unstableFn(): number {
// We simulate a function that can throw unexpected errors
if (Math.round(Math.random() * 1.5) === 0) {
throw new Error("Unexpected error occured");
}
return Math.round((Math.random() + 1) * 99);
}
const fn = new AsyncHooks(unstableFn, [
retry({
// Will retry 4 times
maxAttemps: 4,
}),
]);
await fn.invoke();
Custom ErrorPolicy
You can define an ErrorPolicy
to retry specific error cases:
const fn = new AsyncHooks(unstableFn, [
retry({
maxAttemps: 4,
// Will only retry errors that are not TypeError
errorPolicy: (error) => !(error instanceof TypeError),
}),
]);
Custom BackoffPolicy
You can use custom BackoffPolicy
:
import { TimeSpan } from "@daiso-tech/core/utilities";
const fn = new AsyncHooks(unstableFn, [
retry({
maxAttemps: 4,
// By default a exponential policy is used
backoffPolicy: (attempt: number, _error: unknown) =>
TimeSpan.fromMilliseconds(attempt * 100),
}),
]);
Predefined backoff policies
There are predefined backoff policies that can be used:
Usage example:
import { polynomialBackoffPolicy } from "@daiso-tech/core/async";
const fn = new AsyncHooks(unstableFn, [
retry({
maxAttemps: 4,
// By default a exponential policy is used
backoffPolicy: polynomialBackoffPolicy(),
}),
]);
Dynamically adjusting the settings based on the error:
import { polynomialBackoffPolicy } from "@daiso-tech/core/async";
import { TimeSpan } from "@daiso-tech/core/utilities";
const fn = new AsyncHooks(unstableFn, [
retry({
maxAttemps: 4,
// You can dynamicalyy adapt the setting depending on the error
backoffPolicy: polynomialBackoffPolicy((error) => {
if (isImportant(error)) {
return {
maxDelay: TimeSpan.fromSeconds(30),
degree: 3,
};
}
// If you dont return settings object then default settings will be used.
}),
}),
]);
All predefined backoff policies can be dynamically adjusted based on the error.
Callbacks
You can add callback Invokable
that will be called before execution attempt:
const fn = new AsyncHooks(unstableFn, [
retry({
maxAttemps: 4,
onExecutionAttempt: (data) => console.log(data),
}),
]);
You can add callback Invokable
that will be called before the retry delay starts:
For more details about onExecutionAttempt
callback data, see the OnRetryAttemptData type.
const fn = new AsyncHooks(unstableFn, [
retry({
maxAttemps: 4,
onRetryDelay: (data) => console.log(data),
}),
]);
For more details about onRetryDelay
callback data, see the OnRetryDelayData type.
Timeout
The timeout
middleware automatically aborts functions after a specified time period, throwing an error when aborted.
Usage
import { timeout } from "@daiso-tech/core/async";
import { AsyncHooks, TimeSpan } from "@daiso-tech/core/utilities";
function fetchData(): Promise<Response> {
const response = await fetch("ENDPOINT");
console.log("DONE");
return response;
}
const fn = new AsyncHooks(fetchData, [
timeout({
waitTime: TimeSpan.fromSeconds(2),
}),
]);
await fn.invoke();
Note when a timeout occurs, the function call continues executing in the background and only the Promise
will be aborted.
To ensure correct abortion behavior, provide an AbortSignalBinder
to AsyncHooks
.
For further information about AbortSignalBinder
and AsyncHooks
refer to Hooks documentation.
import { timeout } from "@daiso-tech/core/async";
import { AsyncHooks, TimeSpan } from "@daiso-tech/core/utilities";
function fetchData(signal?: AbortSginal): Promise<Response> {
const response = await fetch("ENDPOINT", {
signal,
});
console.log("DONE");
return response;
}
const fn = new AsyncHooks(
fetchData,
[
timeout({
waitTime: TimeSpan.fromSeconds(2),
}),
],
{
signalBinder: {
getSignal: (args) => args[0],
forwardSignal: (args, signal) => {
args[0] = signal;
},
},
},
);
await fn.invoke();
Callbacks
You can add callback Invokable
that will be called before the timeout occurs.
const fn = new AsyncHooks(
fetchData,
[
timeout({
waitTime: TimeSpan.fromSeconds(2),
onTimeout: (data) => console.log(data),
}),
],
{
signalBinder: {
getSignal: (args) => args[0],
forwardSignal: (args, signal) => {
args[0] = signal;
},
},
},
);
For more details about onTimeout
callback data, see the OnTimeoutData type.
Observe
The observe
middleware tracks an async function's state and runs callbacks when it fails with an error or succeeds:
Usage
import { observe } from "@daiso-tech/core/async";
import { AsyncHooks } from "@daiso-tech/core/utilities";
function unstableFn(): Promise<number> {
// We simulate a function that can throw unexpected errors
if (Math.round(Math.random() * 1.5) === 0) {
throw new Error("Unexpected error occured");
}
return Math.round((Math.random() + 1) * 99);
}
const fn = new AsyncHooks(unstableFn, [
observe({
onStart: (data) => console.log("START:", data),
onSuccess: (data) => console.log("SUCCESS:", data),
onError: (data) => console.error("ERROR:", data),
onFinally: (data) => console.log("END:", data),
}),
]);
await fn.invoke();
-
For more details about
onStart
callback data, see the OnObserveStartData type. -
For more details about
onSuccess
callback data, see the OnObserveSuccessData type. -
For more details about
onError
callback data, see the OnObserveErrorData type. -
For more details about
onFinally
callback data, see the OnObserveFinallyData type.