@snap/camera-kit
Version:
Camera Kit Web
111 lines • 6.1 kB
TypeScript
export type RequestMetadata = {
signal?: AbortSignal | null | undefined;
/**
* When calling a handler, if that handler is part of a handler chain, then by default an abort signal will be
* sent to that handler if another handler higher up in the chain completes first. Usually this is desirable,
* since we know if a handler higher up in the chain has completed and returned a response to its caller, then
* the response from all the handlers below it in the chain will not be used.
*
* But in some cases, a handler in a handler chain wants to call the next handler in the chain as a
* side-effect. If a handler is called as a side-effect, it will not be sent an abort signal when handlers
* higher up in the chain complete.
*
* For example: a caching handler might return a cached value immediately, but then call the next handler as
* a side-effect to fetch an updated value to refresh the cache in the background. In that case, the next
* handler should not be sent an abort signal when the caching handler returns the cached value.
*/
isSideEffect?: boolean;
} | undefined;
export type Handler<Req, Res, Meta extends RequestMetadata> = (req: Req, metadata?: Meta) => Promise<Res>;
export type ChainableHandler<Req, Res, NextReq, NextRes, Meta extends RequestMetadata | undefined> = (next: Handler<NextReq, NextRes, Meta>) => Handler<Req, Res, Meta>;
/**
* Creates a Handler chain – a series of functions composed such that each function may call a supplied `next` function
* which passes execution down the chain. When the final Handler in the chain returns, execution passes back up the
* chain eventually returning to the caller.
*
* Each Handler chain begins with a "raw" Handler – this is a function which takes some request and returns some
* response. A chain is then created by supplying a series of mapping functions – the ChainableHandler type – which will
* be called with the `next` Handler in the chain.
*
* Ex:
* ```ts
* const handler = (request: string, metadata?: RequestMetadata) => Promise.resolve(`Responded to ${request}`)
* const chainable = (next: Handler<string, string>) => (request: string, metadata?: RequestMetadata) => {
* return next(`modified ${request}`, metadata)
* }
*
* const chain = new HandlerChainBuilder(handler)
* .map(chainable)
* .handler
*
* const response = await chain('hello')
* expect(response).toBe('Responded to modified hello; 0')
* ```
* You can largely ignore the `metadata` argument present in the above example. This is the mechanism by which an
* AbortSignal is passed to each Handler in the chain, but the only real requirement when implementing a Handler is
* to pass this argument along to the `next` function. In fact, many Handlers will want to be generic over the type
* of metadata:
* ```ts
* const chainable = <Meta>(next: Handler<string, string, Meta>) => (request: string, metadata: Meta) => {
* return next(`modified ${request}`, metadata)
* }
* ```
* Actually, it's a very good idea for Handlers to be as generic as possible, since that will allow greater re-use. In
* the above example, we don't do anything with the response from `next`, so we can let that be generic, too:
* ```ts
* const chainable = <Res, Meta>(next: Handler<string, Res, Meta>) => (request: string, metadata: Meta) => {
* return next(`modified ${request}`, metadata)
* }
* ```
* Now if some other Handler in the chain decides to return a different response type, our Handler won't require any
* changes to compile.
*
* ---
*
* Since execution passes from handler to handler in the chain, and then back, handlers have the opportunity to modify
* or observe both the request and response. This might be useful for implementing serialization/deserialization, but
* the simplest example that demonstrates this feature is measuring request latency:
* ```ts
* const latencyMeasuringHandler = <Req, Res, Meta>(next: Handler<Req, Res, Meta>) =>
* async (req: Req, metadata: Meta) => {
* const start = performance.now()
* const response = await next(req, metadata)
* const latency = performance.now() - start
* console.log(`latency for request ${request} was ${latency}`)
* return response
* }
* ```
* Execution is first passed to our measuring handler, which marks the `start` timestamp. Then it passes execution on
* down the chain. After a response is received (by some handler down the chain), execution passes back up to our
* handler here, which records the amount of time spent inside `next`.
*
* ---
*
* Handlers may also abort requests. They can do this in two ways:
* 1. Create an `AbortController` and add its `AbortSignal` to the `metadata` object when calling `next`.
* 2. Resolve its returned Promise.
*
* The first approach is straightforward, but the second may benefit from an example – the simplest is a handler which
* will timeout a request:
* ```ts
* const timeoutHandler = <Req, Res, Meta>(next: Handler<Req, Res, Meta>) => (req: Req, metadata: Meta) => {
* return Promise.race([
* next(req, metadata),
* sleep(1000),
* ])
* }
* ```
* The Promise returned by this handler will resolve either when the `next` handler resolves or 1 second has elapsed,
* whichever happens first. If the timeout happens first, we want the `next` handler to recieve an abort signal so that
* it can terminate early (since its result is no longer needed).
*
* HandlerChainBuilder makes this happen by observing when each handler completes, and sending an abort signal to all
* the handlers "downstream" from the aborting handler.
*/
export declare class HandlerChainBuilder<Req, Res, Meta extends RequestMetadata> {
private readonly inner;
constructor(inner: (req: Req, metadata: Meta) => Promise<Res>);
get handler(): Handler<Req, Res, Meta>;
map<PriorReq, PriorRes>(outer: ChainableHandler<PriorReq, PriorRes, Req, Res, Meta>): HandlerChainBuilder<PriorReq, PriorRes, Meta>;
}
//# sourceMappingURL=HandlerChainBuilder.d.ts.map