Skip to content

Commit

Permalink
Merge pull request #1208 from jtfell/feature/component-handler-middle…
Browse files Browse the repository at this point in the history
…ware

[PROPOSAL] Add `component.handler` middleware
  • Loading branch information
aswetlow authored Mar 23, 2022
2 parents f3fdf83 + 4b99bc1 commit 54e487b
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 167 deletions.
395 changes: 252 additions & 143 deletions docs/components.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,14 @@ UNHANDLED() {
```

By default, the current component's `UNHANDLED` gets prioritized over global handlers in other components. Learn more about [`UNHANDLED` prioritization in the routing documentation](./routing.md#unhandled-prioritization).


## Middlewares

The `event.ComponentTreeNode.executeHandler` [event middleware](./middlewares.md#event-middlewares) gets called every time a handler is executed. For example, you can [hook](./hooks.md) into it like this:

```typescript
app.hook('after.event.ComponentTreeNode.executeHandler', (jovo: Jovo): void => {
// ...
});
```
28 changes: 20 additions & 8 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Middleware hooks are the easiest way to extend certain parts of the Jovo Framewo

## Introduction

Jovo hooks allow you to _hook_ into the Jovo middleware architecture to extend or modify the framework without having to change its core code. Learn more about all middlewares in the [RIDR Lifecycle documentation](./ridr-lifecycle.md#middlewares).
Jovo hooks allow you to _hook_ into the Jovo [middleware architecture](./middlewares.md) to extend or modify the framework without having to change its core code. Usually, this is used to hook into the [RIDR Lifecycle](./ridr-lifecycle.md), but it's also possible to hook into [events](./middlewares.md#event-middlewares).

Here are some examples of a hook:

Expand All @@ -19,7 +19,9 @@ Here are some examples of a hook:
import { App, Jovo } from '@jovotech/framework';
// ...

const app = new App({ /* app config */ });
const app = new App({
/* app config */
});

app.hook('<middleware>', (jovo: Jovo): void => {
// ...
Expand Down Expand Up @@ -54,7 +56,9 @@ Here is an example that logs the [`$output` array](./output.md) before it is tur
import { App, Jovo } from '@jovotech/framework';
// ...

const app = new App({ /* app config */ });
const app = new App({
/* app config */
});

app.hook('before.response.output', (jovo: Jovo): void => {
console.log(jovo.$output);
Expand All @@ -67,6 +71,7 @@ app.hook('before.response.output', (jovo) => {
```

Learn more in the sections below:

- [Hooks that use `async` functions](#async-hooks)
- [Hooks outsourced into separate files](#hook-files)

Expand Down Expand Up @@ -106,7 +111,9 @@ import { App } from '@jovotech/framework';
import { sessionCountHook } from './hooks/sessionCount';
// ...

const app = new App({ /* app config */ });
const app = new App({
/* app config */
});

app.hook('before.response.output', sessionCountHook);
```
Expand All @@ -127,7 +134,9 @@ This hook gets executed for every new session before a [handler](https://www.jov
import { App, Jovo } from '@jovotech/framework';
// ...

const app = new App({ /* app config */ });
const app = new App({
/* app config */
});

app.hook('before.dialogue.start', (jovo: Jovo): void => {
if (jovo.$session.isNew) {
Expand Down Expand Up @@ -156,7 +165,6 @@ app.hook('before.dialogue.start', async (jovo: Jovo): Promise<void> => {

If you're used to working with Jovo `v3`: This hook can be used as a replacement of the `NEW_SESSION` handler.


### sessionCount

This hook stores a `sessionCount` variable in the [user database](./data.md#user-data):
Expand All @@ -167,7 +175,9 @@ This hook stores a `sessionCount` variable in the [user database](./data.md#user
import { App, Jovo } from '@jovotech/framework';
// ...

const app = new App({ /* app config */ });
const app = new App({
/* app config */
});

app.hook('before.dialogue.start', (jovo: Jovo): void => {
if (jovo.$session.isNew) {
Expand All @@ -193,7 +203,9 @@ This hook gets executed for new users before a [handler](https://www.jovo.tech/d
import { App, Jovo } from '@jovotech/framework';
// ...

const app = new App({ /* app config */ });
const app = new App({
/* app config */
});

app.hook('before.dialogue.start', (jovo: Jovo): void => {
if (jovo.$user.isNew) {
Expand Down
95 changes: 95 additions & 0 deletions docs/middlewares.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: 'Middlewares'
excerpt: 'Learn how you can extend Jovo by hooking into different types of middlewares.'
---

# Middlewares

Learn how you can extend Jovo by hooking into different types of middlewares.

## Introduction

Jovo offers a middleware architecture that lets you hook into certain steps in the lifecycle. This allows Jovo developers to extend the framework without having to make any adjustments to its core code.

Middlewares can be listened to in two different ways:

- [Hooks](./hooks.md): Lightweight extensions for logging and simple data manipulation
- [Plugins](./plugins.md): Powerful extensions that can hook into multiple middlewares

For example, a hook that does an API call for each new session might look like this:

```typescript
app.hook('before.dialogue.start', async (jovo: Jovo): Promise<void> => {
if (jovo.$session.isNew) {
const response = await someApiCall();
// ...
}
});
```

The most common middlewares to be used are [RIDR middlewares](#ridr-middlewares), but there are also other [types of middlewares](#types-of-middlewares) like [event middlewares](#event-middlewares).

## Types of Middlewares

There are two types of middlewares:

- [RIDR middlewares](#ridr-middlewares): Used to hook into steps across the [RIDR lifecycle](./ridr-lifecycle.md)
- [Event middlewares](#event-middlewares): Used to hook into certain events, for example method calls

### RIDR Middlewares

The [RIDR lifecycle](./ridr-lifecycle.md) (_Request - Interpretation - Dialogue & Logic - Response_) is the main process that determines when each part of the Jovo app is executed.

When extending Jovo, you usually hook into one of the RIDR middlewares that are detailed in the below table:

| Middleware | Description |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| `request.start` | Enters the `request` middleware group |
| `request` | Turns the raw JSON request into a `$request` object |
| `request.end` | Leaves the `request` middleware group with propagated `$request` object |
| `interpretation.start` | Enters the `interpretation` middleware group |
| `interpretation.asr` | ASR/SLU integrations turn speech audio into raw text |
| `interpretation.nlu` | NLU integrations turn raw text into structured input |
| `interpretation.end` | Leaves the `interpretation` middleware group with propagated `$nlu` object |
| `dialogue.start` | Enters the `dialogue` middleware group |
| `dialogue.router` | Uses information from the `interpretation` steps to find the right component and handler |
| `dialogue.logic` | Executes the component and handler logic |
| `dialogue.end` | Leaves the `dialogue` middleware group with propagated `$output` array |
| `response.start` | Enters the `response` middleware group |
| `response.output` | Turns `$output` into a raw JSON response |
| `response.tts` | TTS integrations turn text into speech output |
| `response.end` | Leaves the `response` middleware group with propagated `$response` object |

### Event Middlewares

Event middlewares don't follow a linear process like the [RIDR middlewares](#ridr-middlewares): They get executed whenever a specific method gets called, so this can happen multiple times during one RIDR lifecycle.

There are two types of event middlewares:

- Public methods like [`$resolve`](./handlers.md#resolve-a-component) can be accessed using `event.$resolve`
- Some "under the hood" methods can be accessed using the class name and the method name, for example `event.ComponentTreeNode.executeHandler`

Find all current event middlewares in the table below:

| Middleware | Description |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event.$resolve` | [`$resolve`](./handlers.md#resolve-a-component) is called in a handler |
| `event.$redirect` | [`$redirect`](./handlers.md#redirect-to-components) is called in a handler |
| `event.$delegate` | [`$delegate`](./handlers.md#delegate-to-components) is called in a handler |
| `event.$send` | [`$send`](./output.md#send-a-message) is called in a handler |
| `event.ComponentTreeNode.executeHandler` | This event is called whenever a new handler gets executed. Part of the [`ComponentTreeNode` class](https://github.com/jovotech/jovo-framework/blob/v4/latest/framework/src/ComponentTreeNode.ts). See the [`ComponentTree` section](./components.md#componenttree) for more information. |

## Middleware Features

### Stop the Middleware Execution

Either a [hook](./hooks.md) or a [plugin](./plugins.md) can use `stopMiddlewareExecution` to remove all middlewares from the middleware collection of `HandleRequest` and its plugins. This way, all following middlewares won't be executed.

Here is an example how this could look like for a plugin method (that was registered with a middleware inside `mount`):

```typescript
someMethod(jovo: Jovo): void {
// ...
jovo.$handleRequest.stopMiddlewareExecution();
}
```
2 changes: 1 addition & 1 deletion docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Learn how you can build your own plugins to customize and extend the Jovo Framew

## Introduction

Jovo plugins allow you to hook into the Jovo middleware architecture to extend or modify the framework without having to change its core code. Learn more about all middlewares in the [RIDR Lifecycle documentation](./ridr-lifecycle.md).
Jovo plugins allow you to hook into the Jovo [middleware architecture](./middlewares.md) to extend or modify the framework without having to change its core code. Usually, this is used to hook into the [RIDR Lifecycle](./ridr-lifecycle.md), but it's also possible to hook into [events](./middlewares.md#event-middlewares).

Here are a few use cases where plugins can be helpful:

Expand Down
17 changes: 2 additions & 15 deletions docs/ridr-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ excerpt: 'Learn more about one of the key concepts of Jovo: The RIDR (Request -

# RIDR Lifecycle

Learn more about one of the key concepts of Jovo: The RIDR (Request - Interpretation - Dialogue & Logic - Response) Lifecycle.
Learn more about one of the key concepts of Jovo: The RIDR (_Request - Interpretation - Dialogue & Logic - Response_) Lifecycle.

## Introduction

Expand Down Expand Up @@ -71,7 +71,7 @@ This response is then returned back to the platform.

## Middlewares

For a detailed look into all the framework middlewares that are executed as part of the RIDR Lifecycle, take a look at table below.
For a detailed look into all the RIDR [middlewares](./middlewares.md) that are executed as part of the RIDR Lifecycle, take a look at table below.

| Middleware | Description |
| ---------------------- | ---------------------------------------------------------------------------------------- |
Expand All @@ -90,16 +90,3 @@ For a detailed look into all the framework middlewares that are executed as part
| `response.output` | Turns `$output` into a raw JSON response |
| `response.tts` | TTS integrations turn text into speech output |
| `response.end` | Leaves the `response` middleware group with propagated `$response` object |

### Stopping the Middleware Execution

Either a [hook](./hooks.md) or a [plugin](./plugins.md) can use `stopMiddlewareExecution` to remove all middlewares from the middleware collection of `HandleRequest` and its plugins. This way, all following middlewares won't be executed.

Here is an example how this could look like for a plugin method (that was registered with a middleware inside `mount`):

```typescript
someMethod(jovo: Jovo): void {
// ...
jovo.$handleRequest.stopMiddlewareExecution();
}
```
6 changes: 6 additions & 0 deletions framework/src/ComponentTreeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface ComponentTreeNodeOptions<COMPONENT extends BaseComponent = Base
children?: Array<ComponentConstructor | ComponentDeclaration>;
}

const EXECUTE_HANDLER_MIDDLEWARE = 'event.ComponentTreeNode.executeHandler';

export interface ExecuteHandlerOptions<
COMPONENT extends BaseComponent,
// eslint-disable-next-line @typescript-eslint/ban-types
Expand Down Expand Up @@ -74,6 +76,10 @@ export class ComponentTreeNode<COMPONENT extends BaseComponent = BaseComponent>
if (!componentInstance[handler as keyof COMPONENT]) {
throw new HandlerNotFoundError(componentInstance.constructor.name, handler.toString());
}

// Run any middlewares that are attached to 'event.ComponentTreeNode.executeHandler'
await jovo.$handleRequest.middlewareCollection.run(EXECUTE_HANDLER_MIDDLEWARE, jovo);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (componentInstance as any)[handler](...(callArgs || []));
} catch (e) {
Expand Down
16 changes: 16 additions & 0 deletions framework/src/Jovo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ import { Platform } from './Platform';
import { JovoRoute } from './plugins/RouterPlugin';
import { forEachDeep } from './utilities';

const DELEGATE_MIDDLEWARE = 'event.$delegate';
const RESOLVE_MIDDLEWARE = 'event.$resolve';
const REDIRECT_MIDDLEWARE = 'event.$redirect';
const SEND_MIDDLEWARE = 'event.$send';

export type JovoConstructor<
REQUEST extends JovoRequest,
RESPONSE extends JovoResponse,
Expand Down Expand Up @@ -284,6 +289,8 @@ export abstract class Jovo<

// push the new OutputTemplate(s) to $output
Array.isArray(newOutput) ? this.$output.push(...newOutput) : this.$output.push(newOutput);

await this.$handleRequest.middlewareCollection.run(SEND_MIDDLEWARE, this);
}

async $redirect<
Expand Down Expand Up @@ -323,6 +330,9 @@ export abstract class Jovo<

// update the active component node in handleRequest to keep track of the state
this.$handleRequest.activeComponentNode = componentNode;

await this.$handleRequest.middlewareCollection.run(REDIRECT_MIDDLEWARE, this);

// execute the component's handler
await componentNode.executeHandler({
jovo: this.getJovoReference(),
Expand Down Expand Up @@ -394,6 +404,9 @@ export abstract class Jovo<
});
// update the active component node in handleRequest to keep track of the state
this.$handleRequest.activeComponentNode = componentNode;

await this.$handleRequest.middlewareCollection.run(DELEGATE_MIDDLEWARE, this);

// execute the component's handler
await componentNode.executeHandler({
jovo: this.getJovoReference(),
Expand Down Expand Up @@ -426,6 +439,9 @@ export abstract class Jovo<

// update the active component node in handleRequest to keep track of the state
this.$handleRequest.activeComponentNode = previousComponentNode;

await this.$handleRequest.middlewareCollection.run(RESOLVE_MIDDLEWARE, this);

// execute the component's handler
await previousComponentNode.executeHandler({
jovo: this.getJovoReference(),
Expand Down

0 comments on commit 54e487b

Please sign in to comment.