126 lines
5.6 KiB
Markdown
126 lines
5.6 KiB
Markdown
|
# @cmdln/result
|
||
|
|
||
|
This module implements my error type for Typescript, `Result<T, E>`. After trying out `@snowfrog/result`, I implemented my own for a couple of reasons.
|
||
|
|
||
|
1. The `@snowfrog` package does not play well with async code.
|
||
|
1. The `isOk` and `isErr` methods could have done more in `@snowfrog` to help access values and errors safely with less boiler plate. In the `@snowfrog` code, the guard methods do not narrow in all cases. In particular, the implicit else of a short-circuit pattern doesn't work. This creates toil in that the call has to use `expect`/`expectErr` which reduces readability and with Typescript's flow control analysis shouldn't be necessary.
|
||
|
|
||
|
In addition to `Result`, this package includes some useful facilities for working with that type and converting code that uses thrown exceptions into `Result` returning code.
|
||
|
|
||
|
## Converting Thrown Exceptions
|
||
|
|
||
|
The high order functions `trySync` and `tryAsync` each accept two function as arguments. The first is a no argument closure for running some code that may thrown an exception. The second is a closure accepting an `unknown` error that can then be narrowed, wrapped, and returned to change any thrown exception into an `Err`.
|
||
|
|
||
|
## Chains of Promises and Result
|
||
|
|
||
|
`Result` shines when using its various combinators, like `andThenAsync` and `yield`. These high order functions accepts closures that allow for changing the success and error types to new values of different types and even changing an `Ok` to an `Err` and vice versa.
|
||
|
|
||
|
`Promise` has a similar design and in theory the types work well together. There are some problems due to the special handling `Promise` gets when used with the `await` keyword. The compiler and runtime literally expect a `Promise` and won't accept a shape that is merely assignable. This forces some diagonal code, similar to nested callbacks, to call `Result` combinators from closures passed to `Promise`'s own combinators, especially `then`.
|
||
|
|
||
|
The `resultify` function sugars over the two types to allow use with `await` without the added boiler plate needed to access the `Result` combinators without always fully resolving the promise.
|
||
|
|
||
|
## Example
|
||
|
|
||
|
This example is excerpted from a past project and uses some structured http errors from another package I wrote.
|
||
|
|
||
|
```typescript
|
||
|
import { BaseContext, BaseError, Result } from "@cmdln/error";
|
||
|
import { passOrYield, resultify, trySync } from "@cmdln/result";
|
||
|
import { NextFunction, Request, Response } from "express";
|
||
|
import { FanOutName } from "fanout";
|
||
|
import { IncomingHttpHeaders } from "http";
|
||
|
import { getLogger } from "index";
|
||
|
import { sendMessage } from "producer";
|
||
|
import { Webhook } from "svix";
|
||
|
import { ID_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "./constants";
|
||
|
|
||
|
export async function handleClerkWebhook(
|
||
|
request: Request,
|
||
|
response: Response,
|
||
|
next: NextFunction
|
||
|
) {
|
||
|
const logger = getLogger(__filename);
|
||
|
logger.info("Received Clerk webhook request.");
|
||
|
const { body: payload, headers } = request;
|
||
|
// without resultify here, the result's combinators would have to be nested
|
||
|
// in a .then call
|
||
|
await resultify(tryVerify(payload, headers))
|
||
|
.andThenAsync(async (verified) => {
|
||
|
logger.debug({ verified }, "Verified call from Clerk");
|
||
|
return await sendMessage(FanOutName.Clerk, verified);
|
||
|
})
|
||
|
.yield(() => {
|
||
|
request.log.debug("Webhook successfully received message");
|
||
|
response.status(200).json({ message: "Success" });
|
||
|
next();
|
||
|
})
|
||
|
.yieldErr((cause) => {
|
||
|
request.log.error({ cause }, "Clerk webhook failed");
|
||
|
response.status(400).json({});
|
||
|
next(cause);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function tryVerify(
|
||
|
payload: string,
|
||
|
headers: IncomingHttpHeaders
|
||
|
): Result<unknown> {
|
||
|
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET);
|
||
|
// there is also tryAsync which works the same way for async code
|
||
|
// remember that if your logic in the first argument returns a value, you
|
||
|
// will need to return the call as well; a common mistake is leaving out a
|
||
|
// return or worse an await with tryAsync
|
||
|
return trySync<unknown, BaseError>(
|
||
|
() => {
|
||
|
// a common practice in our code is to use assertion functions in
|
||
|
// trySync/tryAsync since often we are using a client library and need to
|
||
|
// narrow the result; this use case is a little different but a valid
|
||
|
// example of the pattern
|
||
|
assertClerkHeaders(headers);
|
||
|
return wh.verify(payload, headers);
|
||
|
},
|
||
|
// if we know of any thrown errors that are compatible with our Result's
|
||
|
// generic bindings, this helper function saves more boiler plate
|
||
|
passOrYield(VerifyError, (cause) => {
|
||
|
return VerifyError.err(VerifyErrorKind.InvalidSignature, { cause });
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
|
||
|
enum VerifyErrorKind {
|
||
|
InvalidSignature = "Failed to verify signature on call from Clerk",
|
||
|
MissingHeaders = "Missing required headers on call from Clerk",
|
||
|
}
|
||
|
|
||
|
type VerifyContext = BaseContext & {
|
||
|
headers?: unknown;
|
||
|
};
|
||
|
|
||
|
class VerifyError extends BaseError {
|
||
|
static err(kind: VerifyErrorKind, context: VerifyContext) {
|
||
|
return new VerifyError(500, kind, context);
|
||
|
}
|
||
|
|
||
|
get name() {
|
||
|
return VerifyError.name;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function assertClerkHeaders(
|
||
|
headers: IncomingHttpHeaders
|
||
|
): asserts headers is Record<string, string> {
|
||
|
if (
|
||
|
!headers[ID_HEADER] ||
|
||
|
!headers[TIMESTAMP_HEADER] ||
|
||
|
!headers[SIGNATURE_HEADER]
|
||
|
) {
|
||
|
throw VerifyError.err(VerifyErrorKind.MissingHeaders, { headers });
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## To Do
|
||
|
|
||
|
- [ ] Figure out if async and sync combinators can be cleanly merged similar to Jest's signature for its builder HOF's
|
||
|
- [ ] Remove `Async` suffix for methods names, use `Sync` for non-awaitable code since it is likely less frequently used
|