Skip to content

Commit e7321ec

Browse files
committed
feat: Initial implementation
1 parent 2e44260 commit e7321ec

38 files changed

+1532
-17
lines changed

.componentsignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
[]
1+
[
2+
"AsyncHandlerInput",
3+
"AsyncHandlerOutput",
4+
"Error"
5+
]

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,112 @@
11
# AsyncHandler
2+
3+
[![npm version](https://badge.fury.io/js/asynchandler.svg)](https://www.npmjs.com/package/asynchandler)
4+
5+
This library provides a generic interface which you can use for classes,
6+
so they can easily be combined using composite patterns.
7+
An `AsyncHandler` implementation has 3 functions: `canHandle`, `handle`, and `handleSafe`.
8+
9+
`canHandle` is used to determine if a class supports a given input.
10+
In case it does not, it should throw an error.
11+
If a `canHandle` call succeeds, the `handle` function can be called to perform the necessary action.
12+
`handleSafe` is a utility function that combines the above two steps into one.
13+
14+
For example, assume you want to handle an HTTP request,
15+
and have a separate class for every HTTP method.
16+
In the `canHandle` call for each of those,
17+
they would check if the method is the correct one and throw an error if this is not the case.
18+
In the `handle` call, they could then perform the necessary action for this method,
19+
knowing that the request has the correct method.
20+
21+
This library comes with several utility composite handlers that can be used to combine such handlers into one.
22+
These have the same interface, so from the point of view of the caller,
23+
a single HTTP request class, and a composite handler that combines all the HTTP request classes,
24+
look and perform the same.
25+
26+
## Composite handlers
27+
28+
Below is an overview of all composite handlers provided in this library.
29+
30+
### AsyncHandler
31+
32+
The main interface. Although in practice this is actually an abstract class.
33+
This way it can provide a default `canHandle` implementation that always succeeds,
34+
and a `handleSafe` implementation that calls `canHandle`, and then `handle` if the first succeeds.
35+
36+
### BooleanHandler
37+
38+
Takes as input one or more handlers that return a boolean.
39+
This handler will return `true` if any of the input handlers can handle the input and return `true`.
40+
41+
### CachedHandler
42+
43+
Caches the result of the source handler in a [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap).
44+
The input is used as the key to cache with.
45+
Input field names can also be provided to only use a subset of the input.
46+
47+
### ParallelHandler
48+
49+
Runs the `handleSafe` function of all its source handlers simultaneously.
50+
The `canHandle` of this handler will always succeed.
51+
52+
### ProcessHandler
53+
54+
A handler made for worker threads.
55+
It can be used to only run the source handler depending on if it is executed on the primary or a worker thread.
56+
57+
### SequenceHandler
58+
59+
Runs all its handlers sequentially and returns the result of the last one that can handle the input.
60+
The `canHandle` of this handler will always succeed.
61+
62+
### StaticHandler
63+
64+
A handler that always succeeds and returns the provided value.
65+
66+
### StaticThrowHandler
67+
68+
A handler that always throws the provided error.
69+
It can be configured to always succeed `canHandle` and only throw when `handle` is called,
70+
or to throw in both cases.
71+
72+
### UnionHandler
73+
74+
An abstract class that combines the results of all its source handlers in some way.
75+
How these are combined is determined by the abstract `combine` function.
76+
It can be configured to ignore handlers that do not support the input,
77+
and to ignore errors.
78+
79+
#### ArrayUnionHandler
80+
81+
A `UnionHandler` that combines and flattens the results of its source handlers into a single array.
82+
83+
### WaterfallHandler
84+
85+
This handler will call the `handle` function of the first source handler where the `canHandle` call succeeds.
86+
87+
## Utility
88+
89+
Besides the composite handlers, some utility functions and classes are provided.
90+
91+
### findHandler
92+
93+
A function that returns the first handler from a list of handlers where `canHandle` succeeds.
94+
Throws an error if no such handler can be found.
95+
96+
### filterHandlers
97+
98+
Filters a list of handlers to only return those where `canHandle` succeeds.
99+
Throws an error if this would be an empty list.
100+
101+
### ErrorFactory
102+
103+
An interface for a factory that can create errors.
104+
This can be used in combination with a `StaticThrowHandler`.
105+
106+
#### BaseErrorFactory
107+
108+
An `ErrorFactory` that creates a standard Error.
109+
110+
#### ClassErrorFactory
111+
112+
An `ErrorFactory` that reuses the constructor of the input Error.

config/dummy.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"comment": "Empty JSON file to make sure the config folder exits. If you are not using Components.js this can be removed."
3+
}

jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,13 @@ module.exports = {
1919
'/node_modules/',
2020
'/test/',
2121
],
22+
coverageThreshold: {
23+
'./src': {
24+
branches: 100,
25+
functions: 100,
26+
lines: 100,
27+
statements: 100,
28+
},
29+
},
2230
testTimeout: 90000,
2331
};

package.json

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
{
22
"name": "asynchandler",
3-
"version": "1.0.0",
3+
"version": "0.0.1",
44
"license": "MIT",
5+
"repository": "[email protected]:CommunitySolidServer/AsyncHandler.git",
56
"main": "./dist/index.js",
67
"types": "./dist/index.d.ts",
78
"lsd:module": "https://linkedsoftwaredependencies.org/bundles/npm/asynchandler",
89
"lsd:components": "dist/components/components.jsonld",
910
"lsd:contexts": {
10-
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^1.0.0/components/context.jsonld": "dist/components/context.jsonld"
11+
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^0.0.0/components/context.jsonld": "dist/components/context.jsonld"
1112
},
1213
"lsd:importPaths": {
13-
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^1.0.0/components/": "dist/components/",
14-
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^1.0.0/config/": "config/",
15-
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^1.0.0/dist/": "dist/"
14+
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^0.0.0/components/": "dist/components/",
15+
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^0.0.0/config/": "config/",
16+
"https://linkedsoftwaredependencies.org/bundles/npm/asynchandler/^0.0.0/dist/": "dist/"
1617
},
1718
"files": [
1819
"config",
@@ -28,6 +29,9 @@
2829
"release": "commit-and-tag-version",
2930
"test": "jest"
3031
},
32+
"dependencies": {
33+
"global-logger-factory": "^1.0.0"
34+
},
3135
"devDependencies": {
3236
"@commitlint/cli": "^19.4.0",
3337
"@commitlint/config-conventional": "^19.2.2",
@@ -57,9 +61,6 @@
5761
},
5862
"writerOpts": {
5963
"commitsSort": false
60-
},
61-
"skip": {
62-
"tag": true
6364
}
6465
}
6566
}

src/ArrayUnionHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { AsyncHandler, AsyncHandlerOutput } from './AsyncHandler';
2+
import { UnionHandler } from './UnionHandler';
3+
4+
/**
5+
* A utility handler that concatenates the results of all its handlers into a single result.
6+
*/
7+
export class ArrayUnionHandler<T extends AsyncHandler<unknown, unknown[]>> extends UnionHandler<T> {
8+
public constructor(handlers: T[], requireAll?: boolean, ignoreErrors?: boolean) {
9+
super(handlers, requireAll, ignoreErrors);
10+
}
11+
12+
protected async combine(results: AsyncHandlerOutput<T>[]): Promise<AsyncHandlerOutput<T>> {
13+
return results.flat() as AsyncHandlerOutput<T>;
14+
}
15+
}

src/AsyncHandler.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
2+
export type AsyncHandlerInput<T extends AsyncHandler<unknown, unknown>> = Parameters<T['handle']>[0];
3+
export type AsyncHandlerOutput<T extends AsyncHandler<unknown, unknown>> = Awaited<ReturnType<T['handle']>>;
4+
5+
/**
6+
* Simple interface for classes that can potentially handle a specific kind of data asynchronously.
7+
* Actually a class and not an interface to have a base implementation of `canHandle` and `handleSafe`,
8+
* but can also be used as an interface.
9+
*/
10+
export abstract class AsyncHandler<TIn = void, TOut = void> {
11+
/**
12+
* Checks whether the input can be handled by this class.
13+
* If it cannot handle the input, rejects with an error explaining why.
14+
*
15+
* @param input - Input that could potentially be handled.
16+
*
17+
* @returns A promise resolving if the input can be handled, rejecting with an Error if not.
18+
*/
19+
// eslint-disable-next-line unused-imports/no-unused-vars
20+
public async canHandle(input: TIn): Promise<void> {
21+
// Support any input by default
22+
}
23+
24+
/**
25+
* Handles the given input. This may only be called if {@link canHandle} did not reject.
26+
* When unconditionally calling both in sequence, consider {@link handleSafe} instead.
27+
*
28+
* @param input - Input that needs to be handled.
29+
*
30+
* @returns A promise resolving when handling is finished.
31+
*/
32+
public abstract handle(input: TIn): Promise<TOut>;
33+
34+
/**
35+
* Helper function that first runs {@link canHandle} followed by {@link handle}.
36+
* Throws the error of {@link canHandle} if the data cannot be handled,
37+
* or returns the result of {@link handle} otherwise.
38+
*
39+
* @param input - Input data that will be handled if it can be handled.
40+
*
41+
* @returns A promise resolving if the input can be handled, rejecting with an Error if not.
42+
*/
43+
public async handleSafe(input: TIn): Promise<TOut> {
44+
await this.canHandle(input);
45+
return this.handle(input);
46+
}
47+
}

src/BaseErrorFactory.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { ErrorFactory } from './ErrorFactory';
2+
3+
/**
4+
* An {@link ErrorFactory} that generates a basic {@link Error} with the given message.
5+
*
6+
* If no message is provided when calling the `create` function,
7+
* the message that was provided in the constructor will be used.
8+
*/
9+
export class BaseErrorFactory implements ErrorFactory {
10+
protected readonly message?: string;
11+
12+
public constructor(message?: string) {
13+
this.message = message;
14+
}
15+
16+
public create(message?: string): Error {
17+
return new Error(message ?? this.message);
18+
}
19+
}

src/BooleanHandler.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { getLoggerFor } from 'global-logger-factory';
2+
import { AsyncHandler } from './AsyncHandler';
3+
import { filterHandlers } from './HandlerUtil';
4+
5+
/**
6+
* A composite handler that returns true if any of its handlers can handle the input and return true.
7+
* Handler errors are interpreted as false results.
8+
*/
9+
export class BooleanHandler<TIn> extends AsyncHandler<TIn, boolean> {
10+
protected readonly logger = getLoggerFor(this);
11+
12+
protected readonly handlers: AsyncHandler<TIn, boolean>[];
13+
14+
/**
15+
* Creates a new BooleanHandler that stores the given handlers.
16+
*
17+
* @param handlers - Handlers over which it will run.
18+
*/
19+
public constructor(handlers: AsyncHandler<TIn, boolean>[]) {
20+
super();
21+
this.handlers = handlers;
22+
}
23+
24+
public async canHandle(input: TIn): Promise<void> {
25+
// We use this to generate an error if no handler supports the input
26+
await filterHandlers(this.handlers, input);
27+
}
28+
29+
public async handleSafe(input: TIn): Promise<boolean> {
30+
const handlers = await filterHandlers(this.handlers, input);
31+
return promiseSome(handlers.map(async(handler): Promise<boolean> => handler.handle(input)));
32+
}
33+
34+
public async handle(input: TIn): Promise<boolean> {
35+
let handlers: AsyncHandler<TIn, boolean>[];
36+
try {
37+
handlers = await filterHandlers(this.handlers, input);
38+
} catch (error: unknown) {
39+
this.logger.warn('All handlers failed. This might be the consequence of calling handle before canHandle.');
40+
throw new Error('All handlers failed', { cause: error });
41+
}
42+
return promiseSome(handlers.map(async(handler): Promise<boolean> => handler.handle(input)));
43+
}
44+
}
45+
46+
function noop(): void {}
47+
48+
/**
49+
* A function that simulates the Array.some behaviour but on an array of Promises.
50+
* Returns true if at least one promise returns true.
51+
* Returns false if all promises return false or error.
52+
*
53+
* @remarks
54+
*
55+
* Predicates provided as input must be implemented considering
56+
* the following points:
57+
* 1. if they throw an error, it won't be propagated;
58+
* 2. throwing an error should be logically equivalent to returning false.
59+
*/
60+
export async function promiseSome(predicates: Promise<boolean>[]): Promise<boolean> {
61+
return new Promise((resolve): void => {
62+
function resolveIfTrue(value: boolean): void {
63+
if (value) {
64+
resolve(true);
65+
}
66+
}
67+
Promise.all(predicates.map(async(predicate): Promise<void> => predicate.then(resolveIfTrue, noop)))
68+
.then((): void => resolve(false), noop);
69+
});
70+
}

0 commit comments

Comments
 (0)