10000 Koa style middleware: Hooks for any asynchronous method · Issue #932 · feathersjs/feathers · GitHub
[go: up one dir, main page]

Skip to content

Koa style middleware: Hooks for any asynchronous method #932

@daffl

Description

@daffl

Hooks are a powerful way to control the flow of asynchronous methods. Koa popularized a new kind of middleware that allows an even more flexible approach to control this flow. I propose to adapt this pattern as a new async hook type.

Problem

Currently, Feathers hooks are split into three types, before, after and error:

feathers-hooks-old

While this pattern works quite well, it is a little cumbersome to implement functionality that needs to have access to the before, after and/or error flow at the same time. For example functionality for a profiler that logs the method runtime or extracting and storing data (e.g. associations) that will be used after the method call returns requires a before and an after hook.

Proposal

With async/await now being fully supported by all recent versions of Node - and the ability to fall back to Promises for Node 6 - we now have a powerful new way to create middleware that can control the whole flow within a single function.

This pattern has been implemented already in KoaJS for processing HTTP requests. However, similar to what I wrote before about Express style middleware, this also doesn't have to be limited to the HTTP request/response cycle but can apply to any asynchronous function, including Feathers services. Basically this new kind of hook will wrap around the following hooks and the service method call like this:

feathers-hooks-2 1

How it works

Additionally to the hook context async hooks also get an asynchronous next function that controls passing the flow to the next hook in the chain.

The following example registers an application wide before, after and error logger and profiler for all service method calls:

app.hooks({
  async: [
    async (context, next) => {
      try {
        const start = new Date().getTime();

        console.log(`Before calling ${context.method} on ${context.path}`);

        await next();

        console.log(`Successfully called ${context.method} on ${context.path}`, context.result);
        console.log('Calling ${context.method} on ${context.path} took ${new Date().getTime() - start}ms');
        return context;
      } catch(error) {
        console.error(`Error in ${context.method} on ${context.path}`, error.message);
        // Not re-throwing would swallow the error
        throw error;
      }
    }
  ]
});

The following example extracts the address association for a user create method and saves it with the userId reference:

app.service('users').hooks({
  async: {
    create: [
      async (context, next) => {
        const { app, data } = context;
        const { address } = data;

        context.data = _.omit(data, 'address');

        // Get the result from the context returned by the next hook
        // usually the same as accessing `context.result` after the `await next()` call
        // but more flexible and functional
        const { result } = await next();

        // Get the created user id
        const userId = context.result.id;

        // Create address with user id reference
        await app.service('addresses').create(Object.assign({
          userId
        }, address));

        return context;
      }
    ]
  }
});

async hook type

The core of async hooks would be implemented as an individual module that allows to add hooks to any asynchronous method. This has been inspired by the great work @bertho-zero has been doing in #924 to allow using hooks on other service methods. I created a first prototype in https://github.com/daffl/async-hooks (test case can be found here). It uses Koa's middleware composition module koa-compose.

async hooks would be implemented as an additional hook type but all hook dispatching will be done using the new middleware composition library. Existing before after and error hooks will be wrapped and registered in a backwards compatible way:

app.service('users').hooks({
  async: {
    all: [],
    create: [],
    find: []
    // etc.
  },

  // Continue using all your existing hooks here
  before: {
    all: [],
    find: []
    // etc.
  },

  after: {},

  error: {}
});

async hooks will have precedence so the execution order would be async -> before -> after. Even when only using the old hooks there are several advantages:

  • Better stack traces
  • Slightly better performance
  • Less custom code to maintain and the potential to collaborate with the Koa project

Migration path

There will be two breaking changes:

  1. The currently still supported (but undocumented and not recommended) function(context, next) signature for existing before, after and error hooks will no longer work.
  2. koa-compose does not allow to replace the entire context. Returning the context is still possible but other hooks have to retrieve it from the await next(); call with const ctx = await next();.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0