-
-
Notifications
You must be signed in to change notification settings - Fork 797
Description
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:
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:
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:
- The currently still supported (but undocumented and not recommended)
function(context, next)signature for existingbefore,afteranderrorhooks will no longer work. koa-composedoes not allow to replace the entire context. Returning the context is still possible but other hooks have to retrieve it from theawait next();call withconst ctx = await next();.

