Skip to content

Commit

Permalink
feat: Event Emitter middleware (#615)
Browse files Browse the repository at this point in the history
* Add event-emitter middleware

* Commit yarn.lock

* Add author to package.json

* Remove CHANGELOG.md

* Bump up node version

* Adjust initial version
  • Loading branch information
DavidHavl authored Jul 7, 2024
1 parent 9bf6c4b commit 53b4f33
Show file tree
Hide file tree
Showing 10 changed files with 940 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-lies-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/event-emitter': major
---

Full release of Event Emitter middleware for Hono
25 changes: 25 additions & 0 deletions .github/workflows/ci-event-emitter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci-event-emitter
on:
push:
branches: [main]
paths:
- 'packages/event-emitter/**'
pull_request:
branches: ['*']
paths:
- 'packages/event-emitter/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/event-emitter
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"build:typia-validator": "yarn workspace @hono/typia-validator build",
"build:swagger-ui": "yarn workspace @hono/swagger-ui build",
"build:esbuild-transpiler": "yarn workspace @hono/esbuild-transpiler build",
"build:event-emitter": "yarn workspace @hono/event-emitter build",
"build:oauth-providers": "yarn workspace @hono/oauth-providers build",
"build:react-renderer": "yarn workspace @hono/react-renderer build",
"build:auth-js": "yarn workspace @hono/auth-js build",
Expand Down
359 changes: 359 additions & 0 deletions packages/event-emitter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
# Event Emitter middleware for Hono

Minimal, lightweight and edge compatible Event Emitter middleware for [Hono](https://github.com/honojs/hono).

It enables event driven logic flow in hono applications (essential in larger projects or projects with a lot of interactions between features).

Inspired by event emitter concept in other frameworks such
as [Adonis.js](https://docs.adonisjs.com/guides/emitter), [Nest.js](https://docs.nestjs.com/techniques/events), [Hapi.js](https://github.com/hapijs/podium), [Laravel](https://laravel.com/docs/11.x/events), [Sails.js](https://sailsjs.com/documentation/concepts/extending-sails/hooks/events), [Meteor](https://github.com/Meteor-Community-Packages/Meteor-EventEmitter) and others.


## Installation

```sh
npm install @hono/event-emitter
# or
yarn add @hono/event-emitter
# or
pnpm add @hono/event-emitter
# or
bun install @hono/event-emitter
```


## Usage

#### There are 2 ways you can use this with Hono:

### 1. As Hono middleware

```js
// event-handlers.js

// Define event handlers
export const handlers = {
'user:created': [
(c, payload) => {} // c is current Context, payload will be correctly inferred as User
],
'user:deleted': [
(c, payload) => {} // c is current Context, payload will be inferred as string
],
'foo': [
(c, payload) => {} // c is current Context, payload will be inferred as { bar: number }
]
}

// You can also define single event handler as named function
// export const userCreatedHandler = (c, user) => {
// // c is current Context, payload will be inferred as User
// // ...
// console.log('New user created:', user)
// }

```

```js
// app.js

import { emitter } from '@hono/event-emitter'
import { handlers, userCreatedHandler } from './event-handlers'
import { Hono } from 'hono'

// Initialize the app with emitter type
const app = new Hono()

// Register the emitter middleware and provide it with the handlers
app.use('*', emitter(handlers))

// You can also setup "named function" as event listener inside middleware or route handler
// app.use((c, next) => {
// c.get('emitter').on('user:created', userCreatedHandler)
// return next()
// })

// Routes
app.post('/user', async (c) => {
// ...
// Emit event and pass current context plus the payload
c.get('emitter').emit('user:created', c, user)
// ...
})

app.delete('/user/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload
c.get('emitter').emit('user:deleted', c, id)
// ...
})

export default app
```

The emitter is available in the context as `emitter` key, and handlers (when using named functions) will only be subscribed to events once, even if the middleware is called multiple times.

As seen above (commented out) you can also subscribe to events inside middlewares or route handlers, but you can only use named functions to prevent duplicates!

### 2 Standalone


```js
// events.js

import { createEmitter } from '@hono/event-emitter'

// Define event handlers
export const handlers = {
'user:created': [
(c, payload) => {} // c is current Context, payload will be whatever you pass to emit method
],
'user:deleted': [
(c, payload) => {} // c is current Context, payload will be whatever you pass to emit method
],
'foo': [
(c, payload) => {} // c is current Context, payload will be whatever you pass to emit method
]
}

// Initialize emitter with handlers
const emitter = createEmitter(handlers)

// And you can add more listeners on the fly.
// Here you CAN use anonymous or closure function because .on() is only called once.
emitter.on('user:updated', (c, payload) => {
console.log('User updated:', payload)
})

export default emitter

```

```js
// app.js

import emitter from './events'
import { Hono } from 'hono'

// Initialize the app
const app = new Hono()

app.post('/user', async (c) => {
// ...
// Emit event and pass current context plus the payload
emitter.emit('user:created', c, user)
// ...
})

app.delete('/user/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload
emitter.emit('user:deleted', c, id )
// ...
})

export default app
```

## Typescript

### 1. As hono middleware

```ts
// types.ts

import type { Emitter } from '@hono/event-emitter'

export type User = {
id: string,
title: string,
role: string
}

export type AvailableEvents = {
// event key: payload type
'user:created': User;
'user:deleted': string;
'foo': { bar: number };
};

export type Env = {
Bindings: {};
Variables: {
// Define emitter variable type
emitter: Emitter<AvailableEvents>;
};
};


```

```ts
// event-handlers.ts

import { defineHandlers } from '@hono/event-emitter'
import { AvailableEvents } from './types'

// Define event handlers
export const handlers = defineHandlers<AvailableEvents>({
'user:created': [
(c, user) => {} // c is current Context, payload will be correctly inferred as User
],
'user:deleted': [
(c, payload) => {} // c is current Context, payload will be inferred as string
],
'foo': [
(c, payload) => {} // c is current Context, payload will be inferred as { bar: number }
]
})

// You can also define single event handler as named function using defineHandler to leverage typings
// export const userCreatedHandler = defineHandler<AvailableEvents, 'user:created'>((c, user) => {
// // c is current Context, payload will be inferred as User
// // ...
// console.log('New user created:', user)
// })

```

```ts
// app.ts

import { emitter, type Emitter, type EventHandlers } from '@hono/event-emitter'
import { handlers, userCreatedHandler } from './event-handlers'
import { Hono } from 'hono'
import { Env } from './types'

// Initialize the app
const app = new Hono<Env>()

// Register the emitter middleware and provide it with the handlers
app.use('*', emitter(handlers))

// You can also setup "named function" as event listener inside middleware or route handler
// app.use((c, next) => {
// c.get('emitter').on('user:created', userCreatedHandler)
// return next()
// })

// Routes
app.post('/user', async (c) => {
// ...
// Emit event and pass current context plus the payload (User type)
c.get('emitter').emit('user:created', c, user)
// ...
})

app.delete('/user/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload (string)
c.get('emitter').emit('user:deleted', c, id)
// ...
})

export default app
```

### 2. Standalone:

```ts
// types.ts

type User = {
id: string,
title: string,
role: string
}

type AvailableEvents = {
// event key: payload type
'user:created': User;
'user:updated': User;
'user:deleted': string,
'foo': { bar: number };
}

```

```ts
// events.ts

import { createEmitter, defineHandlers, type Emitter, type EventHandlers } from '@hono/event-emitter'
import { AvailableEvents } from './types'

// Define event handlers
export const handlers = defineHandlers<AvailableEvents>({
'user:created': [
(c, user) => {} // c is current Context, payload will be correctly inferred as User
],
'user:deleted': [
(c, payload) => {} // c is current Context, payload will be inferred as string
],
'foo': [
(c, payload) => {} // c is current Context, payload will be inferred as { bar: number }
]
})

// You can also define single event handler using defineHandler to leverage typings
// export const userCreatedHandler = defineHandler<AvailableEvents, 'user:created'>((c, payload) => {
// // c is current Context, payload will be correctly inferred as User
// // ...
// console.log('New user created:', payload)
// })

// Initialize emitter with handlers
const emitter = createEmitter(handlers)

// emitter.on('user:created', userCreatedHandler)

// And you can add more listeners on the fly.
// Here you can use anonymous or closure function because .on() is only called once.
emitter.on('user:updated', (c, payload) => { // Payload will be correctly inferred as User
console.log('User updated:', payload)
})

export default emitter

```

```ts
// app.ts

import emitter from './events'
import { Hono } from 'hono'

// Initialize the app
const app = new Hono()

app.post('/user', async (c) => {
// ...
// Emit event and pass current context plus the payload (User)
emitter.emit('user:created', c, user)
// ...
})

app.delete('/user/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload (string)
emitter.emit('user:deleted', c, id )
// ...
})

export default app
```



### NOTE:

When assigning event handlers inside of middleware or route handlers, don't use anonymous or closure functions, only named functions!
This is because anonymous functions or closures in javascript are created as new object every time and therefore can't be easily checked for equality/duplicates.


For more usage examples, see the [tests](src/index.test.ts) or [Hono REST API starter kit](https://github.com/DavidHavl/hono-rest-api-starter)

## Author

- David Havl - <https://github.com/DavidHavl>

## License

MIT
Loading

0 comments on commit 53b4f33

Please sign in to comment.