@35up/tslib-utils
v4.1.5
Published
Suite of utils for javascript applications
Downloads
44
Readme
tslib-utils
Suite of utils for javascript applications
Operation and AsyncOperation
Operation and AsyncOperation are utility classes that helps to gracefully
handle situations when we need to perform some operation and handle its status,
including error. Another of their purposes is to make the handling of errors more
explicit and harder to forget.
Operation has 3 variants:
- Pending Operations: They have a
pendingstatus while we get a response. - Successful Operations: They have a
successstatus when the request succeeds. - Failed Operations: They have a
failstatus when the request fails.
There is also the concept of a ResolvedOperation, which is an operation that
can only be "Successful" or "Failed".
Operation is the base monad, containing the status, the data in case of
success and the error in case of failure.
AsyncOperation is a Promise-like wrapper on top of Operation, made to
simplify the use of operation in async code, as well as to catch some common
edge-cases related. In instance of an AsyncOperation is still a Promise, that
resolves into an ResolvedOperation, and therefore, to access the data, error or status,
it needs to be treated as a Promise.
Here an example on its intended use:
// service.ts
import { AsyncOperation } from '@35up/tslib-utils';
function expensiveAsyncQuery(...args): AsyncOperation<QueryResult> {
return AsyncOperation.try(async () => {
// Do some stuff
});
}
// component.tsx
import { Operation } from '@35up/tslib-utils';
import { expensiveAsyncQuery } from './service';
import { Error } from './utilities';
export function Component({ args }) {
const [ queryResult, setQueryResult ] = useState(Operation.makePending());
useEffect(async () => {
setQueryResult(
await expensiveAsyncQuery(...args)
.mapFailure('Failed to performe expencive operation')
);
}, [args]);
return (
<div>
...
{Operation.isSuccess(queryResult) && (
<div>The result was: {queryResult.data}</div>
)}
...
{Operation.isFailure(queryResult) && (
<Error>{queryResult.error.message}</Error>
)}
</div>
);
}
Most important features
try [Operation and AsyncOperation]
Wraps a callback in a try catch so that it can be safely executed.
When callback executes without errors, then it will return a "Successful
Operation" with the data field containing the returned value of the
provided callback (or resolved value in the case of AsyncOperation). When the
callback generate an exception (or rejection in the case of AsyncOperation),
it will return a "Failed Operation" with an error field containing the Error.
const result = AsyncOperation.try(async () => fetch('/url'));
if (Operation.isFailure(result)) {
console.log(result.data); // fetched data
} else {
console.error(result.error);
}Utilities [only AsyncOperation]
Try offer convenience methods to reduce boilerplate. These are provided as the first argument of the callback.
AsyncOperation.try(async (utilities) => {
// Some complicated logic
});unwrapData
It allows you to access the data contained in an ResolvedOperation. When the
ResolvedOperation is a "Failure", it throws; But that is not an issue, as
try will catch it, and result in a "Failed Operation".
When provided with a promise of an ResolvedOperation or an AsyncOperation,
it will return a promise that resolves in the data of the ResolvedOperation.
AsyncOperation.try(async ({ unwrap }) => {
const a = await unwrapData(getA());
const b = unwrapData(await getB());
return a / b;
});vs
AsyncOperation.try(async () => {
const a = await getA();
const b = await getB();
if (Operation.isFailure(a)) {
throw a.error;
}
if (Operation.isFailure(b)) {
throw b.error;
}
return a.data / b.data;
});wrap [AsyncOperation]
Wrap creates an AsyncOperation from an Operation or a promise that
resolves in an Operation. Basically when operation is already finished we can
create an instance from the final result.
import { Operation } from './operation';
import { AsyncOperation } from './async-operation';
const operation = await AsyncOperation.wrap(Operation.makeSuccess(7));
console.log(result.data); // 7
console.log(Operation.isSuccess(result)); // true
// or
const promise = Promise.resolve(Operation.makeSuccess(7));
const asyncOperation = AsyncOperation.wrap(promise);
console.log(result.data); // 7
console.log(Operation.isSuccess(result)); // trueaggregate [Operation and AsyncOperation]
Converts an array of Operations (and AsyncOperations in the case of
AsyncOperation.wrap) into a single operation containing an array of all the
results. In case of one of them failing, it returns "Failed Operation" containing
the Error. In case that multiple fail, it will return a "Failed Operation"
with an AggregateError containing all the underlying errors. This is
intended to be used similarly to Promise.all
const results = await AsyncOperation.aggregate([
Promise.resolve(Operation.makeSuccess('a')),
Promise.resolve(Operation.makeSuccess(1)),
]);
console.log(results); // ['a', 1]Checking the status
Use static methods of Operation class to check the status. AsyncOperations
don't have status out of them selves, but as they are fancy promises, they
resolve into an Operation.
Example of checking the status on an Operation
import { Operation } from './operation';
const operation = Operation.makePending();
console.assert(Operation.isPending(operation) === true);
console.assert(Operation.isSuccess(operation) === false);
console.assert(Operation.isFailure(operation) === false);Example of checking the status on an AsyncOperation
const operation = Operation.makePending();
operation = await AsyncOperation.try(async () => fetch('/url'));
console.assert(Operation.isPending(operation) === false);
console.assert(Operation.isSuccess(operation) === true);
console.assert(Operation.isFailure(operation) === false);Create Operation with specific status
There are static methods to create Operations and AsyncOperations in all its
variants.
For Operations
const emptySuccess = Operation.makeSuccess('test');
const success = Operation.makeSuccess('test');
const failure = Operation.makeFailure(new Error('fail'));
const panding = Operation.makePending();For AsyncOperations
const emptyAsyncSuccess = AsyncOperation.makeSuccess();
const asyncSuccess = AsyncOperation.makeSuccess('test');
const asyncFailure = AsyncOperation.makeFailure(new Error('fail'));Note: there is no makePending for AsyncOperation because it does not make
sense to have a promise return a "Pending Operation" in the majority of valid
use cases. We decided to forbid that in our types, to avoid common bugs.
Chaining executions
Often times we need to use the result of async operation in another operation.
There is a way to compose operations in a monad-like way. For that you can
create a chain of operations using mapSuccess to plug into successful result
or mapFailure to plug to a failed result.
AsyncOperation.try(async () => fetch('/settings'))
.mapSuccess((settings) => fetch(`/products?lang=${settings.language}`))
.mapSuccess((products) => orders.filter(p => p.isStock))
.mapFailure('Failed to get data');Note: mapFailure is not necessary to handle errors. It is just a way to
specify a bit more concrete error message in case of failure. The original error
in this case would be just added as a cause of the returned error object.
Handling Errors
To access the data of an Operation, we have to first check its status. This
forces us to be more mindful of its error state. This is an example on how we
would expect errors being handled:
const result = await someExepensiveAsyncOperation()
.mapFailure('Could not perform the expensive operation correctly');
if (Operation.isFailure(result)) {
// do something to recover or bail out
} else {
// do someting with the data
}
wrapHttpAsOperation
Most of the services that deal with backend have the same structure:
- Make fetch to the server
- Validate response against schema
- Handle errors
On order to reduce the boilerplate there is a util function
wrapHttpAsOperation that does all of the above. You just need to pass a url,
validation schema and parameters.
Without wrapHttpAsOperation:
try {
const endpoint = '/price-overrides/';
const result: TServerPriceOverrides = schema.parse(
await post(endpoint, data),
{path: [endpoint]},
);
return makeSuccess(result || null);
} catch (e) {
return makeFail(e as Error);
}With wrapHttpAsOperation:
const wrapperPost = wrapHttpAsOperation(post);
return wrappedPost('/price-overrides/', schema, data);