UNPKG

@snap/camera-kit

Version:
111 lines 6.1 kB
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