metcoder95 / fastify-racing

Cancel any running operation at the right time on your request handler
MIT License
12 stars 1 forks source link
abort-signal abortcontroller fastify fastify-library fastify-plugin http http2 racing

fastify-racing

CI CodeQL version


fastify-racing is a plugin which allows you handle possible client request abortions by exposing an AbortSignal instance that will be aborted just and only when the client has closed the request abruptly (e.g. by closing the browser tab).

How it works?

On every request and after a first invocation, the plugin well schedule event listeners to the close event triggered by the Socket instance attached to the request object.

Along with that, the plugin will instanciate and cache an AbortController instance for each request.

When the close event is triggered, the plugin will check if the AbortSignal instance is already aborted, and if not will abort it using the AbortController instance.

Is guaranteed that one and just one AbortController and AbortSignal will be made per request.

If the request was not aborted during its lifetime, the plugin will remove the AbortController and AbortSignal from the cache. This by scheduling a hook-handler on the hook onResponse.

If the request aborted, the same hook will be used for cleaning resources.

A WeakMap is used under the hood for caching, ensuring that the AbortController and AbortSignal instances can be unlinked if not needed anymore, and for instance GC'ed.

Setup

Install by running npm install fastify-racing.

Then register the plugin to your fastify instance:

const fastify = require('fastify')({
  logger: true
})

fastify.register(require('fastify-racing'), {
    handleError: true,
})

Options

On Setup

How to use it?

There are two ways to use this plugin:

Promise

It will return a promise that will be resolved when the request is aborted. It will be resolved with the result of the abort event object of the AbortSignal instance. This only if no cb has been passed as argument.

It supports an object as argument:

JavaScript

app.get('/', async (req, _reply) => {
    const signal = req.race()
    const result = await Promise.race([signal, asyncOp(signal)])

    if (result.type === 'aborted') return ''
    else return `${result}-world`
})

TypeScript

app.post('/', (request: FastifyRequest, reply: FastifyReply) => {
    const signal = req.race()
    const result: AbortEvent | unknown = await Promise.race([signal, asyncOp(signal)])

    if ((<AbortEvent>result).type === 'aborted') return ''
    else return `${result}-world`
});

Callback

If a callback is provided, no promise will be scheduled/returned during the lifetime of the request.

JavaScript

app.get('/', (req, reply) => {
    const signal = req.race((evt) => {
        const result = result.type === 'aborted' ? '' : `${result}-world`

        reply.send(result)
    })
})

TypeScript

app.post('/', (request: FastifyRequest, reply: FastifyReply) => {
    const signal = req.race((evt: AbortEvent) => {
        reply.send('')
    })
});

Type Definitions

interface AbortEvent {
    type: 'abort' | string;
    reason?: FastifyError | Error
}

interface FastifyRacing {
  handleError?: boolean;
  onRequestClosed?: (evt: AbortEvent) => void;
}

interface FastifyInstance {
    race(cb: FastifyRacing['onRequestClosed']): void
    race(opts: Omit<FastifyRacing, 'onRequestClosed'>): Promise<AbortEvent>
    race(): Promise<AbortEvent>
}

See test for more examples.