-
Notifications
You must be signed in to change notification settings - Fork 44
Description
While I've been experimenting with #480 I've been thinking about how we can create a "middleware" style feature using the hooks that were recently added to the worker. The ideas here span both the worker and the library repos.
Evolving the types
Looking at the types as defined here
azure-functions-nodejs-worker/types-core/index.d.ts
Lines 52 to 62 in f6d3625
function registerHook(hookName: 'preInvocation', callback: PreInvocationCallback): Disposable; | |
function registerHook(hookName: 'postInvocation', callback: PostInvocationCallback): Disposable; | |
function registerHook(hookName: 'appStart', callback: AppStartCallback): Disposable; | |
function registerHook(hookName: 'appTerminate', callback: AppTerminateCallback): Disposable; | |
function registerHook(hookName: string, callback: HookCallback): Disposable; | |
type HookCallback = (context: HookContext) => void | Promise<void>; | |
type PreInvocationCallback = (context: PreInvocationContext) => void | Promise<void>; | |
type PostInvocationCallback = (context: PostInvocationContext) => void | Promise<void>; | |
type AppStartCallback = (context: AppStartContext) => void | Promise<void>; | |
type AppTerminateCallback = (context: AppTerminateContext) => void | Promise<void>; |
There isn't much in common with them, the type of hook to be registered are strings in the overload and the types for each callback don't have a common base type. As a result, trying to create a generic middleware
function in the library project can be difficult without a lot of overloads there to ensure that the type system is happy.
Using an enum
or union for the name of the hook would make it simpler to have other places within the code be aware of valid hook names.
For the callbacks, it'd be helpful if they inherited a base callback type as it can make for simpler type signatures elsewhere where you might want to create an abstraction layer over the hooks (again, something that could be exposed in the new programming model).
Adding more richness to HookContext
Is it possible to add some proper types to the PreInvocationContext
and PostInvocationContext
, particularly for the inputs
? That would make it easier to understand what the inputs are (obviously you'd have to do your own explicit type checking as your inputs are inferred at runtime)? Maybe that could be a generic argument so that the usage of the hook could be explicit about the input types it's expecting?
I'm thinking in the new programming model we could support "hook filters", where you specify that you only want the hook to run on a certain trigger type, or when an input contains some value, etc.
For this we might also need more information on the HookContext, particularly the pre/post versions.
Do we know the "id" of the Function being executed? Can that be made available on the context object?
What about the trigger? Can the trigger be an explicit field so that you can write filters against "only HTTP triggered Functions"?
Pass data from hooks to Functions
One use-case for hooks is to be able to add additional stuff to a Function, say a CosmosClient
so that the Function can do operations against CosmosDB that aren't possible via a binding.
Initially, it might seem like that's something you could use the hookData
property for, but that's only persisted across the hooks, and not provided to the Function.
It is possible to use the InvocationContext
as a way to pass stuff, but it's typed as unknown
(probably so it can support either programming model) so you have to cast it to something else to work with. Here's how I did it with the new programming model:
registerHook("preInvocation", async (context) => {
const client = new CosmosClient(process.env.CosmosConnectionString);
const { database } = await client.databases.createIfNotExists({
id: "TodoList",
});
const { container } = await database.containers.createIfNotExists({
id: "Items",
partitionKey: { paths: ["/id"] },
});
const ctx = context.invocationContext as any;
ctx.extraInputs.set("cosmosContainer", container);
});
But if instead we had an explicity method, like setFunctionData
on the hook context object, then we could write hooks that are simplified across the different programming models.
Ability to cancel Function runs
When thinking about hooks as middleware, there are reasons that you might want to cancel an execution of a Function if a pre-condition isn't met. An example of this would be for validation of an incoming HTTP trigger payload. While this could be done at a Function level, being able to use hooks as a way to make it generic really increases their usefulness.
This can be achieved by replacing the functionCallback
on the hook context with a new function, but it's a bit hacky (and if anything changed in how the worker uses that property it may break).
Having an abort
method on the InvocationContext would be a way to bail out.