feat(event-emitter): Enable invoking asynchronous handlers (#649)

* Add new emitAsync method, memory leak prevention and significantly improve README.md

* Adjust ci yarn install command

* Commit yarn.lock

* Run changeset

* Revert ci yarn install command

* Move context parameter to the first position in the `emit` method

* Fix test
pull/680/head
David Havl 2024-08-06 14:05:35 +02:00 committed by GitHub
parent d3e7037e61
commit 0b6d821c11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1040 additions and 344 deletions

View File

@ -0,0 +1,12 @@
---
'@hono/event-emitter': major
---
### Added:
- New `emitAsync` method to the EventEmitter to enable invoking asynchronous handlers.
- Added prevention for potential memory leak when adding handlers inside of middleware via `on` method.
- Introduced new option of EventEmitter `maxHandlers` that limits number of handlers that can be added to a single event.
- Significantly improved documentation.
### Changed:
- Moved context parameter to the first position in the `emit` method.

View File

@ -1,12 +1,32 @@
# Event Emitter middleware for Hono
### Minimal, lightweight and edge compatible Event Emitter middleware for [Hono](https://github.com/honojs/hono).
Minimal, lightweight and edge compatible Event Emitter middleware for [Hono](https://github.com/honojs/hono).
## Table of Contents
1. [Introduction](#introduction)
2. [Installation](#installation)
3. [Usage Examples](#usage-examples)
- [1. As Hono middleware](#1-as-hono-middleware)
- [2. Standalone](#2-standalone)
4. [API Reference](#api-reference)
- [emitter](#emitter)
- [createEmitter](#createemitter)
- [defineHandler](#definehandler)
- [defineHandlers](#definehandlers)
- [Emitter API Documentation](#emitter)
5. [Types](#types)
It enables event driven logic flow in hono applications (essential in larger projects or projects with a lot of interactions between features).
## Introduction
This library provides an event emitter middleware for Hono, allowing you to easily implement and manage event-driven architectures in your Hono applications.
It enables event driven logic flow, allowing you to decouple your code and make it more modular and maintainable.
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.
as [Adonis.js](https://docs.adonisjs.com/guides/emitter), [Nest.js](https://docs.nestjs.com/techniques/events), [Hapi.js](https://github.com/hapijs/podium), [Meteor](https://github.com/Meteor-Community-Packages/Meteor-EventEmitter) and others.
See [FAQ](#faq) bellow for some common questions.
For more usage examples, see the [tests](src/index.test.ts) or my [Hono REST API starter kit](https://github.com/DavidHavl/hono-rest-api-starter)
## Installation
@ -20,7 +40,6 @@ pnpm add @hono/event-emitter
bun install @hono/event-emitter
```
## Usage
#### There are 2 ways you can use this with Hono:
@ -33,21 +52,21 @@ bun install @hono/event-emitter
// Define event handlers
export const handlers = {
'user:created': [
(c, payload) => {} // c is current Context, payload will be correctly inferred as User
(c, payload) => {} // c is current Context, payload is whatever the emit method passes
],
'user:deleted': [
(c, payload) => {} // c is current Context, payload will be inferred as string
async (c, payload) => {} // c is current Context, payload is whatever the emit method passes
],
'foo': [
(c, payload) => {} // c is current Context, payload will be inferred as { bar: number }
]
(c, payload) => {} // c is current Context, payload is whatever the emit method passes
],
}
// 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
// export const fooHandler = (c, payload) => {
// // c is current Context, payload is whatever the emit method passes
// // ...
// console.log('New user created:', user)
// console.log('New foo created:', payload)
// }
```
@ -56,42 +75,43 @@ export const handlers = {
// app.js
import { emitter } from '@hono/event-emitter'
import { handlers, userCreatedHandler } from './event-handlers'
import { handlers, fooHandler } 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))
app.use(emitter(handlers))
// You can also setup "named function" as event listener inside middleware or route handler
// You can also add event listener inside middleware or route handler, but please only use named functions to prevent duplicates and memory leaks!
// app.use((c, next) => {
// c.get('emitter').on('user:created', userCreatedHandler)
// c.get('emitter').on('foo', fooHandler)
// return next()
// })
// Routes
app.post('/user', async (c) => {
app.post('/users', (c) => {
// ...
// Emit event and pass current context plus the payload
c.get('emitter').emit('user:created', c, user)
c.get('emitter').emit(c, 'user:created', user)
// ...
})
app.delete('/user/:id', async (c) => {
app.delete('/users/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload
c.get('emitter').emit('user:deleted', c, id)
// Emit event asynchronpusly and pass current context plus the payload
await c.get('emitter').emitAsync(c, 'user:deleted', 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.
The emitter is available in the context as `emitter` key.
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!
As seen above (commented out) you can also subscribe to events inside middlewares or route handlers,
but because middlewares are called on every request, you can only use named functions to prevent duplicates or memory leaks!
### 2 Standalone
@ -107,46 +127,43 @@ export const handlers = {
(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
async (c, payload) => {} // c is current Context, payload will be whatever you pass to emit method
]
}
// Initialize emitter with handlers
const emitter = createEmitter(handlers)
const ee = 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)
})
// ee.on('foo', async (c, payload) => {
// console.log('New foo created:', payload)
// })
export default emitter
export default ee
```
```js
// app.js
import emitter from './events'
import { Hono } from 'hono'
import ee from './events'
// Initialize the app
const app = new Hono()
app.post('/user', async (c) => {
app.post('/users', async (c) => {
// ...
// Emit event and pass current context plus the payload
emitter.emit('user:created', c, user)
ee.emit(c, 'user:created', user)
// ...
})
app.delete('/user/:id', async (c) => {
app.delete('/users/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload
emitter.emit('user:deleted', c, id )
await ee.emitAsync(c, 'user:deleted', id )
// ...
})
@ -198,18 +215,15 @@ export const handlers = defineHandlers<AvailableEvents>({
(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 }
async (c, payload) => {} // c is current Context, payload will be inferred as string
]
})
// 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
// export const fooHandler = defineHandler<AvailableEvents, 'foo'>((c, payload) => {
// // c is current Context, payload will be inferred as { bar: number }
// // ...
// console.log('New user created:', user)
// console.log('Foo:', payload)
// })
```
@ -218,7 +232,7 @@ export const handlers = defineHandlers<AvailableEvents>({
// app.ts
import { emitter, type Emitter, type EventHandlers } from '@hono/event-emitter'
import { handlers, userCreatedHandler } from './event-handlers'
import { handlers, fooHandler } from './event-handlers'
import { Hono } from 'hono'
import { Env } from './types'
@ -226,11 +240,11 @@ import { Env } from './types'
const app = new Hono<Env>()
// Register the emitter middleware and provide it with the handlers
app.use('*', emitter(handlers))
app.use(emitter(handlers))
// You can also setup "named function" as event listener inside middleware or route handler
// You can also add event listener inside middleware or route handler, but please only use named functions to prevent duplicates and memory leaks!
// app.use((c, next) => {
// c.get('emitter').on('user:created', userCreatedHandler)
// c.get('emitter').on('foo', fooHandler)
// return next()
// })
@ -238,20 +252,25 @@ app.use('*', emitter(handlers))
app.post('/user', async (c) => {
// ...
// Emit event and pass current context plus the payload (User type)
c.get('emitter').emit('user:created', c, user)
c.get('emitter').emit(c, 'user:created', 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)
await c.get('emitter').emitAsync(c, 'user:deleted', id)
// ...
})
export default app
```
The emitter is available in the context as `emitter` key.
As seen above (the commented out 'foo' event) you can also subscribe to events inside middlewares or route handlers,
but because middlewares are called on every request, you can only use named functions to prevent duplicates or memory leaks!
### 2. Standalone:
```ts
@ -285,39 +304,32 @@ export const handlers = defineHandlers<AvailableEvents>({
(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 }
async (c, payload) => {} // c is current Context, payload will be inferred as string
]
})
// 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)
// })
// export const fooHandler = defineHandler<AvailableEvents, 'foo'>((c, payload) => {})
// Initialize emitter with handlers
const emitter = createEmitter(handlers)
const ee = createEmitter(handlers)
// emitter.on('user:created', userCreatedHandler)
// ee.on('foo', fooHandler)
// 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
ee.on('foo', async (c, payload) => { // Payload will be correctly inferred as User
console.log('User updated:', payload)
})
export default emitter
export default ee
```
```ts
// app.ts
import emitter from './events'
import ee from './events'
import { Hono } from 'hono'
// Initialize the app
@ -326,33 +338,409 @@ 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)
ee.emit(c, 'user:created', user)
// ...
})
app.delete('/user/:id', async (c) => {
// ...
// Emit event and pass current context plus the payload (string)
emitter.emit('user:deleted', c, id )
ee.emit(c, 'user:deleted', id )
// ...
})
export default app
```
## API Reference
### emitter
Creates a Hono middleware that adds an event emitter to the context.
```ts
function emitter<EPMap extends EventPayloadMap>(
eventHandlers?: EventHandlers<EPMap>,
options?: EventEmitterOptions
): MiddlewareHandler
```
#### Parameters
- `eventHandlers` - (optional): An object containing initial event handlers. Each key is event name and value is array of event handlers. Use `defineHandlers` function to create fully typed event handlers.
- `options` - (optional): An object containing options for the emitter. Currently, the only option is `maxHandlers`, which is the maximum number of handlers that can be added to an event. The default is `10`.
#### Returns
A Hono middleware function that adds an `Emitter` instance to the context under the key 'emitter'.
#### Example
```ts
app.use(emitter(eventHandlers));
```
### createEmitter
Creates new instance of event emitter with provided handlers. This is usefull when you want to use the emitter as standalone feature instead of Hono middleware.
```ts
function createEmitter<EPMap extends EventPayloadMap>(
eventHandlers?: EventHandlers<EPMap>,
options?: EventEmitterOptions
): Emitter<EPMap>
```
#### Parameters
- `eventHandlers` - (optional): An object containing initial event handlers. Each key is event name and value is array of event handlers.
- `options` - (optional): An object containing options for the emitter. Currently, the only option is `maxHandlers`, which is the maximum number of handlers that can be added to an event. The default is `10`.
#### Returns
An `Emitter` instance:
#### Example
```ts
const ee = createEmitter(eventHandlers);
```
### defineHandler
A utility function to define a typed event handler.
```ts
function defineHandler<EPMap extends EventPayloadMap, Key extends keyof EPMap, E extends Env = Env>(
handler: EventHandler<EPMap[Key], E>,
): EventHandler<EPMap[Key], E>
```
#### Parameters
- `handler`: The event handler function to be defined.
#### Type parameters
- `EPMap`: The available event key to payload map i.e.: `type AvailableEvents = { 'user:created': { name: string } };`.
- `Key`: The key of the event type.
- `E`: (optional) - The Hono environment, so that the context within the handler has the right info.
#### Returns
The same event handler function with proper type inference.
#### Example
```ts
type AvailableEvents = {
'user:created': { name: string };
};
const handler = defineHandler<AvailableEvents, 'user:created'>((c, payload) => {
console.log('New user created:', payload)
})
```
### defineHandlers
A utility function to define multiple typed event handlers.
```ts
function defineHandlers<EPMap extends EventPayloadMap, E extends Env = Env>(
handlers: { [K in keyof EPMap]?: EventHandler<EPMap[K], E>[] },
): { [K in keyof EPMap]?: EventHandler<EPMap[K], E>[] }
```
#### Parameters
- `handlers`: An object containing event handlers for multiple event types/keys.
#### Type parameters
- `EPMap`: The available event key to payload map i.e.: `type AvailableEvents = { 'user:created': { name: string } };`.
- `E`: (optional) - The Hono environment, so that the context within the handler has the right info.
#### Returns
The same handlers object with proper type inference.
#### Example
```ts
type AvailableEvents = {
'user:created': { name: string };
};
const handlers = defineHandlers<AvailableEvents>({
'user:created': [
(c, payload) => {
console.log('New user created:', pyload)
}
]
})
```
## Emitter instance methods
The `Emitter` interface provides methods for managing and triggering events. Here's a detailed look at each method:
### on
Adds an event handler for the specified event key.
#### Signature
```ts
function on<Key extends keyof EventPayloadMap>(
key: Key,
handler: EventHandler<EventPayloadMap[Key]>
): void
```
#### Parameters
- `key`: The event key to listen for. Must be a key of `EventHandlerPayloads`.
- `handler`: The function to be called when the event is emitted. If using within a Hono middleware or request handler, do not use anonymous or closure functions!
It should accept two parameters:
- `c`: The current Hono context object.
- `payload`: The payload passed when the event is emitted. The type of the payload is inferred from the `EventHandlerPayloads` type.
#### Returns
`void`
#### Example
Using outside the Hono middleware or request handler:
```ts
type AvailableEvents = {
'user:created': { name: string };
};
const ee = createEmitter<AvailableEvents>();
// If adding event handler outside of Hono middleware or request handler, you can use both, named or anonymous function.
ee.on('user:created', (c, user) => {
console.log('New user created:', user)
})
```
Using within Hono middleware or request handler:
```ts
type AvailableEvents = {
'user:created': { name: string };
};
// Define event handler as named function, outside of the Hono middleware or request handler to prevent duplicates/memory leaks
const namedHandler = defineHandler<AvailableEvents, 'user:created'>((c, user) => {
console.log('New user created:', user)
})
app.use(emitter<AvailableEvents>());
app.use((c, next) => {
c.get('emitter').on('user:created', namedHandler)
return next()
})
```
### off
Removes an event handler for the specified event key.
#### Signature
```ts
function off<Key extends keyof EventPayloadMap>(
key: Key,
handler?: EventHandler<EventPayloadMap[Key]>
): void
```
#### Parameters
- `key`: The event key to remove the handler from. Must be a key of `EventPayloadMap`.
- `handler` (optional): The specific handler function to remove. If not provided, all handlers for the given key will be removed.
#### Returns
`void`
#### Example
```ts
type AvailableEvents = {
'user:created': { name: string };
};
const ee = createEmitter<AvailableEvents>();
const logUser = defineHandler<AvailableEvents, 'user:created'>((c, user) => {
console.log(`User: ${user.name}`);
});
ee.on('user:created', logUser);
// Later, to remove the specific handler:
ee.off('user:created', logUser);
// Or to remove all handlers for 'user:created':
ee.off('user:created');
```
### NOTE:
### emit
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.
Synchronously emits an event with the specified key and payload.
#### Signature
```ts
emit<Key extends keyof EventPayloadMap>(
c: Context,
key: Key,
payload: EventPayloadMap[Key]
): void
```
#### Parameters
- `c`: The current Hono context object.
- `key`: The event key to emit. Must be a key of `EventPayloadMap`.
- `payload`: The payload to pass to the event handlers. The type of the payload is inferred from the `EventPayloadMap` type.
#### Returns
`void`
#### Example
```ts
app.post('/users', (c) => {
const user = { name: 'Alice' };
c.get('emitter').emit(c, 'user:created', user);
});
```
### emitAsync
Asynchronously emits an event with the specified key and payload.
#### Signature
```ts
emitAsync<Key extends keyof EventPayloadMap>(
c: Context,
key: Key,
payload: EventPayloadMap[Key],
options?: EmitAsyncOptions
): Promise<void>
```
#### Parameters
- `c`: The current Hono context object.
- `key`: The event key to emit. Must be a key of `EventPayloadMap`.
- `payload`: The payload to pass to the event handlers. The type of the payload is inferred from the `EventPayloadMap` type.
- `options` (optional): An object containing options for the asynchronous emission.
Currently, the only option is `mode`, which can be `'concurrent'` (default) or `'sequencial'`.
- The `'concurrent'` mode will call all handlers concurrently (at the same time) and resolve or reject (with aggregated errors) after all handlers settle.
- The `'sequencial'` mode will call handlers one by one and resolve when all handlers are done or reject when the first error is thrown, not executing rest of the handlers.
#### Returns
`Promise<void>`
#### Example
```ts
app.post('/users', async (c) => {
const user = { name: 'Alice' };
await c.get('emitter').emitAsync(c, 'user:created', user);
// await c.get('emitter').emitAsync(c, 'user:created', user, { mode: 'sequencial' });
});
```
## Types
### EventKey
A string literal type representing an event key.
```ts
type EventKey = string | symbol
```
### EventHandler
A function type that handles an event.
```ts
type EventHandler<T, E extends Env = Env> = (c: Context<E>, payload: T) => void | Promise<void>
```
### EventHandlers
An object type containing event handlers for multiple event types/keys.
```ts
type EventHandlers<T, E extends Env = Env> = { [K in keyof T]?: EventHandler<T[K], E>[] }
```
### EventPayloadMap
An object type containing event keys and their corresponding payload types.
```ts
type EventPayloadMap = Record<EventKey, any>
```
### EventEmitterOptions
An object type containing options for the `Emitter` class.
```ts
type EventEmitterOptions = { maxHandlers?: number };
```
### EmitAsyncOptions
An object type containing options for the `emitAsync` method.
```ts
type EmitAsyncOptions = {
mode?: 'concurrent' | 'sequencial'
}
```
### Emitter
An interface representing an event emitter.
```ts
interface Emitter<EventPayloadMap> {
on<Key extends keyof EventPayloadMap>(key: Key, handler: EventHandler<EventPayloadMap[Key]>): void;
off<Key extends keyof EventPayloadMap>(key: Key, handler?: EventHandler<EventPayloadMap[Key]>): void;
emit<Key extends keyof EventPayloadMap>(c: Context, key: Key, payload: EventPayloadMap[Key]): void;
emitAsync<Key extends keyof EventPayloadMap>(
c: Context,
key: Key,
payload: EventPayloadMap[Key],
options?: EmitAsyncOptions
): Promise<void>;
}
```
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)
## FAQ
### What the heck is event emitter and why should I use it?
Event emitter is a pattern that allows you to decouple your code and make it more modular and maintainable.
It's a way to implement the observer pattern in your application.
It's especially useful in larger projects or projects with a lot of interactions between features.
Just imagine you have a user registration feature, and you want to send a welcome email after the user is created. You can do this by emitting an event `user:created` and then listen to this event in another part of your application (e.g. email service).
### How is this different to the built-in EventEmitter in Node.js?
The build-in EventEmitter has huge API surface, weak TypeScript support and does only synchronous event emitting. Hono's event emitter is designed to be minimal, lightweight, edge compatible and fully typed. Additionally, it supports async event handlers.
### Is there a way to define event handlers with types?
Yes, you can use `defineHandlers` and `defineHandler` functions to define event handlers with types. This way you can leverage TypeScript's type inference and get better type checking.
### Does it support async event handlers?
Yes, it does. You can use async functions as event handlers and emit the events using `emitAsync` method.
### What happens if I emit an event that has no handlers?
Nothing. The event will be emitted, but no handlers will be called.
### Using `emitAsync` function, what happens if one or more of the handlers reject?
- If using `{ mode = 'concurrent' }` in the options (which is the default), it will call all handlers concurrently (at the same time) and resolve or reject (with aggregated errors) after all handlers settle.
- If using `{ mode = 'sequencial' }` in the options, it will call handlers one by one and resolve when all handlers are done or reject when the first error is thrown, not executing rest of the handlers.
### Is it request scoped?
No, by design it's not request scoped. The same Emitter instance is shared across all requests.
This aproach prevents memory leaks (especially when using closures or dealing with large data structures within the handlers) and additional strain on Javascript garbage collector.
### Why can't I use anonymous functions or closures as event handlers when adding them inside of middleware?
This is because middleware or request handlers run repeatedly on every request, and because anonymous functions are created as new unique object in memory every time,
you would be instructing the event emitter to add new handler for same key every time the request/middleware runs.
Since they are each different objects in memory they can't be checked for equality and would result in memory leaks and duplicate handlers.
You should use named functions if you really want to use the `on()` method inside of middleware or request handler.
## Author
- David Havl - <https://github.com/DavidHavl>
David Havl <https://github.com/DavidHavl>
## License

View File

@ -36,8 +36,11 @@
"hono": "*"
},
"devDependencies": {
"hono": "^3.11.7",
"hono": "^4.3.6",
"tsup": "^8.0.1",
"vitest": "^1.0.4"
"vitest": "^1.6.0"
},
"engines": {
"node": ">=16.0.0"
}
}

View File

@ -1,243 +1,436 @@
import { Hono } from 'hono';
import { expect, vi } from 'vitest';
import { emitter, createEmitter, type Emitter, type EventHandlers, defineHandler, defineHandlers } from '../src';
import { Hono } from 'hono'
import type {Context} from 'hono'
import { describe, expect, it, vi } from 'vitest'
import { createEmitter, defineHandler, defineHandlers, emitter } from './index'
import type {Emitter} from './index' // Adjust the import path as needed
describe('EventEmitter', () => {
describe('Used inside of route handlers', () => {
it('Should work when subscribing to events inside of route handler', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
describe('Event Emitter Middleware', () => {
describe('createEmitter', () => {
it('should create an emitter with initial handlers', () => {
type EventPayloadMap = {
test: { id: string; text: string }
}
const handlers = {
test: [vi.fn()],
}
const ee = createEmitter<EventPayloadMap>(handlers)
expect(ee).toBeDefined()
expect(ee.emit).toBeDefined()
expect(ee.on).toBeDefined()
expect(ee.off).toBeDefined()
expect(ee.emitAsync).toBeDefined()
})
const handler = defineHandler<EventHandlerPayloads, 'todo:created'>((_c, _payload) => {});
it('should create an emitter without initial handlers', () => {
const ee = createEmitter()
expect(ee).toBeDefined()
})
const spy = vi.fn(handler);
it('should allow adding and removing handlers', () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
const handler = vi.fn()
ee.on('test', handler)
ee.emit({} as Context, 'test', 'payload')
expect(handler).toHaveBeenCalledWith({}, 'payload')
const app = new Hono<Env>();
ee.off('test', handler)
ee.emit({} as Context, 'test', 'payload')
expect(handler).toHaveBeenCalledTimes(1)
})
app.use('*', emitter());
it('should remove all handlers for an event when no handler is specified', () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
const handler1 = vi.fn()
const handler2 = vi.fn()
ee.on('test', handler1)
ee.on('test', handler2)
ee.off('test')
ee.emit({} as Context, 'test', 'payload')
expect(handler1).not.toHaveBeenCalled()
expect(handler2).not.toHaveBeenCalled()
})
app.use((c, next) => {
c.get('emitter').on('todo:created', spy);
return next();
});
it('should emit events to all registered handlers', () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
const handler1 = vi.fn()
const handler2 = vi.fn()
ee.on('test', handler1)
ee.on('test', handler2)
ee.emit({} as Context, 'test', 'payload')
expect(handler1).toHaveBeenCalledWith({}, 'payload')
expect(handler2).toHaveBeenCalledWith({}, 'payload')
})
let currentContext = null;
app.post('/todo', (c) => {
currentContext = c;
c.get('emitter').emit('todo:created', c, { id: '2', text: 'Buy milk' });
return c.json({ message: 'Todo created' });
});
it('should not add the same named function handler multiple times', () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
const handler = vi.fn()
ee.on('test', handler)
ee.on('test', handler)
ee.emit({} as Context, 'test', 'payload')
expect(handler).toHaveBeenCalledTimes(1)
})
const res = await app.request('http://localhost/todo', { method: 'POST' });
expect(res).not.toBeNull();
expect(res.status).toBe(200);
expect(spy).toHaveBeenCalledWith(currentContext, { id: '2', text: 'Buy milk' });
});
it('should emit async events concurrently', async () => {
type EventPayloadMap = {
test: { id: string }
}
const ee = createEmitter<EventPayloadMap>()
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const handler1 = vi.fn(
defineHandler<EventPayloadMap, 'test'>(async (_c, _payload) => {
await delay(100)
})
)
const handler2 = vi.fn(
defineHandler<EventPayloadMap, 'test'>(async (_c, _payload) => {
await delay(100)
})
)
it('Should not subscribe same handler to same event twice inside of route handler', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
ee.on('test', handler1)
ee.on('test', handler2)
const handler = defineHandler<EventHandlerPayloads, 'todo:created'>((_c, _payload) => {});
const start = Date.now()
await ee.emitAsync({} as Context, 'test', { id: '123' }, { mode: 'concurrent' })
const end = Date.now()
const spy = vi.fn(handler);
// The total time should be close to 100ms (since handlers run concurrently)
// We'll allow a small margin for execution time
expect(end - start).toBeLessThan(150)
const app = new Hono<Env>();
expect(handler1).toHaveBeenCalledWith(expect.anything(), { id: '123' })
expect(handler2).toHaveBeenCalledWith(expect.anything(), { id: '123' })
expect(handler1).toHaveBeenCalledTimes(1)
expect(handler2).toHaveBeenCalledTimes(1)
})
app.use('*', emitter());
it('should emit async events sequentially', async () => {
type EventPayloadMap = {
test: { id: string }
}
const ee = createEmitter<EventPayloadMap>()
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const handler1 = vi.fn(
defineHandler<EventPayloadMap, 'test'>(async (_c, _payload) => {
await delay(101)
})
)
const handler2 = vi.fn(
defineHandler<EventPayloadMap, 'test'>(async (_c, _payload) => {
await delay(101)
})
)
ee.on('test', handler1)
ee.on('test', handler2)
const start = Date.now()
await ee.emitAsync({} as Context, 'test', { id: '123' }, { mode: 'sequencial' })
const end = Date.now()
app.use((c, next) => {
c.get('emitter').on('todo:created', spy);
return next();
});
// The total time should be close to 200ms (since handlers run sequentially)
// We'll allow a small margin for execution time
expect(end - start).toBeGreaterThanOrEqual(200)
expect(end - start).toBeLessThan(250)
app.post('/todo', (c) => {
c.get('emitter').emit('todo:created', c, { id: '2', text: 'Buy milk' });
return c.json({ message: 'Todo created' });
});
expect(handler1).toHaveBeenCalledWith(expect.anything(), { id: '123' })
expect(handler2).toHaveBeenCalledWith(expect.anything(), { id: '123' })
expect(handler1).toHaveBeenCalledTimes(1)
expect(handler2).toHaveBeenCalledTimes(1)
})
await app.request('http://localhost/todo', { method: 'POST' });
await app.request('http://localhost/todo', { method: 'POST' });
await app.request('http://localhost/todo', { method: 'POST' });
expect(spy).toHaveBeenCalledTimes(3);
});
it('should throw AggregateError when async handlers fail using emitAsync with concurent mode', async () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
const handler1 = vi.fn().mockRejectedValue(new Error('Error 1'))
const handler2 = vi.fn().mockRejectedValue(new Error('Error 2'))
ee.on('test', handler1)
ee.on('test', handler2)
await expect(ee.emitAsync({} as Context, 'test', 'payload')).rejects.toThrow(AggregateError)
try {
await ee.emitAsync({} as Context, 'test', 'payload', { mode: 'concurrent' })
// Should not reach here
expect(true).toBe(false)
} catch (error) {
expect((error as AggregateError).errors).toHaveLength(2)
expect((error as AggregateError).errors[0].message).toBe('Error 1')
expect((error as AggregateError).errors[1].message).toBe('Error 2')
}
})
it('Should work assigning event handlers via middleware', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
};
it('should stop execution on first error in async handlers fail using emitAsync with sequential mode', async () => {
type EventPayloadMap = {
test: { id: string }
}
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
const ee = createEmitter<EventPayloadMap>()
const handlers = defineHandlers<EventHandlerPayloads>({
const handler1 = vi.fn(
defineHandler<EventPayloadMap, 'test'>(async () => {
throw new Error('Error 1')
})
)
const handler2 = vi.fn(
defineHandler<EventPayloadMap, 'test'>(async () => {
// This should not be called
})
)
ee.on('test', handler1)
ee.on('test', handler2)
try {
await ee.emitAsync({} as Context, 'test', { id: '789' }, { mode: 'sequencial' })
// Should not reach here
expect(true).toBe(false)
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect((error as Error).message).toBe('Error 1')
}
expect(handler1).toHaveBeenCalledWith(expect.anything(), { id: '789' })
expect(handler1).toHaveBeenCalledTimes(1)
expect(handler2).not.toHaveBeenCalled()
})
it('should throw TypeError when adding a non-function handler', () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
expect(() => ee.on('test', 'not a function' as any)).toThrow(TypeError)
})
it('should throw RangeError when max handlers limit is reached', () => {
type EventPayloadMap = { test: string }
const emitter = createEmitter<EventPayloadMap>({}, { maxHandlers: 3 })
emitter.on('test', vi.fn())
emitter.on('test', vi.fn())
emitter.on('test', vi.fn())
expect(() => emitter.on('test', vi.fn())).toThrow(RangeError)
})
it('should use default max handlers limit of 10 when not specified', () => {
type EventPayloadMap = { testEvent: string }
const emitter = createEmitter<EventPayloadMap>()
for (let i = 0; i < 10; i++) {
emitter.on('testEvent', vi.fn())
}
expect(() => emitter.on('testEvent', vi.fn())).toThrow(RangeError)
})
it('should allow different events to have their own handler counts', () => {
type EventPayloadMap = { test1: string; test2: string }
const emitter = createEmitter<EventPayloadMap>({}, { maxHandlers: 2 })
emitter.on('test1', vi.fn())
emitter.on('test1', vi.fn())
emitter.on('test2', vi.fn())
expect(() => emitter.on('test1', vi.fn())).toThrow(RangeError)
expect(() => emitter.on('test2', vi.fn())).not.toThrow()
})
it('should include event key in error message when limit is reached', () => {
type EventPayloadMap = { specificEvent: string }
const emitter = createEmitter<EventPayloadMap>({}, { maxHandlers: 1 })
emitter.on('specificEvent', vi.fn())
expect(() => emitter.on('specificEvent', vi.fn())).toThrow(/specificEvent/)
})
it('should allow setting custom max handlers limit', () => {
type EventPayloadMap = { test: string }
const emitter = createEmitter<EventPayloadMap>({}, { maxHandlers: 5 })
for (let i = 0; i < 5; i++) {
emitter.on('test', vi.fn())
}
expect(() => emitter.on('test', vi.fn())).toThrow(RangeError)
})
it('should do nothing when emitting an event with no handlers', () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
expect(() => ee.emit({} as Context, 'test', 'payload')).not.toThrow()
})
it('should do nothing when emitting an async event with no handlers', async () => {
type EventPayloadMap = {
test: string
}
const ee = createEmitter<EventPayloadMap>()
await expect(ee.emitAsync({} as Context, 'test', 'payload')).resolves.toBeUndefined()
})
})
describe('emitter middleware', () => {
it('should add emitter to context', async () => {
type EventPayloadMap = {
test: string
}
const middleware = emitter<EventPayloadMap>()
const context = {
set: vi.fn(),
} as unknown as Context
const next = vi.fn()
await middleware(context, next)
expect(context.set).toHaveBeenCalledWith('emitter', expect.any(Object))
expect(next).toHaveBeenCalled()
})
it('should create emitter with provided handlers', async () => {
const handler = vi.fn()
type EventPayloadMap = {
test: string
}
const middleware = emitter<EventPayloadMap>({ test: [handler] })
let capturedEmitter: Emitter<EventPayloadMap> | undefined
const context = {
set: vi.fn().mockImplementation((key, value) => {
if (key === 'emitter') {
capturedEmitter = value
}
}),
} as unknown as Context
const next = vi.fn()
await middleware(context, next)
expect(context.set).toHaveBeenCalledWith('emitter', expect.any(Object))
expect(capturedEmitter).toBeDefined()
capturedEmitter?.emit({} as Context, 'test', 'payload')
expect(handler).toHaveBeenCalledWith({}, 'payload')
})
})
describe('defineHandler', () => {
it('should return the provided handler', () => {
type EventPayloadMap = {
test: number
}
const handler = (_c: Context, _payload: number) => {}
const definedHandler = defineHandler<EventPayloadMap, 'test'>(handler)
expect(definedHandler).toBe(handler)
})
})
describe('defineHandlers', () => {
it('should return the provided handlers object', () => {
const handlers = {
test: [(_c: Context, _payload: number) => {}],
}
const definedHandlers = defineHandlers(handlers)
expect(definedHandlers).toBe(handlers)
})
})
describe('type safety', () => {
it('should enforce correct types for event payloads', () => {
type EventPayloadMap = {
numberEvent: number
objectEvent: { id: string }
}
const ee = createEmitter<EventPayloadMap>()
// These should compile without errors
ee.on('numberEvent', (_c, payload) => {
const _num: number = payload
})
ee.on('objectEvent', (_c, payload) => {
const _id: string = payload.id
})
// @ts-expect-error - payload should be a number
ee.emit('numberEvent', {} as Context, 'not a number')
// @ts-expect-error - payload should be an object with an id property
ee.emit('objectEvent', {} as Context, { wrongKey: 'value' })
// These should compile without errors
ee.emit({} as Context, 'numberEvent', 42)
ee.emit({} as Context, 'objectEvent', { id: 'test' })
})
})
describe('Hono request flow', () => {
it('should work when assigning event handlers via middleware', async () => {
type EventPayloadMap = {
'todo:created': { id: string; text: string }
}
type Env = { Variables: { emitter: Emitter<EventPayloadMap> } }
const handlers = defineHandlers<EventPayloadMap>({
'todo:created': [vi.fn((_c, _payload) => {})],
});
})
const app = new Hono<Env>();
const app = new Hono<Env>()
app.use('*', emitter(handlers));
let currentContext = null;
app.use(emitter(handlers))
const ee = createEmitter<{ sdf: string; adsf: number }>()
ee.on('adsf', vi.fn())
let currentContext = null
app.post('/todo', (c) => {
currentContext = c;
c.get('emitter').emit('todo:created', c, { id: '2', text: 'Buy milk' });
return c.json({ message: 'Todo created' });
});
currentContext = c
c.get('emitter').emit(c, 'todo:created', { id: '2', text: 'Buy milk' })
return c.json({ message: 'Todo created' })
})
const res = await app.request('http://localhost/todo', { method: 'POST' });
expect(res).not.toBeNull();
expect(res.status).toBe(200);
expect(handlers['todo:created']?.[0]).toHaveBeenCalledWith(currentContext, { id: '2', text: 'Buy milk' });
});
});
const res = await app.request('http://localhost/todo', { method: 'POST' })
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(handlers['todo:created']?.[0]).toHaveBeenCalledWith(currentContext, {
id: '2',
text: 'Buy milk',
})
})
describe('Used as standalone', () => {
it('Should work assigning event handlers via createEmitter function param', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
'todo:deleted': { id: string };
};
it('should work when assigning async event handlers via middleware', async () => {
type EventPayloadMap = {
'todo:created': { id: string; text: string }
}
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
type Env = { Variables: { emitter: Emitter<EventPayloadMap> } }
const handlers: EventHandlers<EventHandlerPayloads> = {
'todo:created': [vi.fn((_payload) => {})],
};
const handlers = defineHandlers<EventPayloadMap>({
'todo:created': [vi.fn(async (_c, _payload) => {})],
})
const ee = createEmitter<EventHandlerPayloads>(handlers);
const app = new Hono<Env>()
const todoDeletedHandler = vi.fn(defineHandler<EventHandlerPayloads, 'todo:deleted'>((_c, _payload) => {}));
app.use(emitter(handlers))
ee.on('todo:deleted', todoDeletedHandler);
let currentContext = null
app.post('/todo', async (c) => {
currentContext = c
await c.get('emitter').emitAsync(c, 'todo:created', { id: '2', text: 'Buy milk' })
return c.json({ message: 'Todo created' })
})
const app = new Hono<Env>();
let todoCreatedContext = null;
app.post('/todo', (c) => {
todoCreatedContext = c;
ee.emit('todo:created', c, { id: '2', text: 'Buy milk' });
return c.json({ message: 'Todo created' });
});
let todoDeletedContext = null;
app.delete('/todo/123', (c) => {
todoDeletedContext = c;
ee.emit('todo:deleted', c, { id: '3' });
return c.json({ message: 'Todo deleted' });
});
const res = await app.request('http://localhost/todo', { method: 'POST' });
expect(res).not.toBeNull();
expect(res.status).toBe(200);
expect(handlers['todo:created']?.[0]).toHaveBeenCalledWith(todoCreatedContext, { id: '2', text: 'Buy milk' });
const res2 = await app.request('http://localhost/todo/123', { method: 'DELETE' });
expect(res2).not.toBeNull();
expect(res2.status).toBe(200);
expect(todoDeletedHandler).toHaveBeenCalledWith(todoDeletedContext, { id: '3' });
});
it('Should work assigning event handlers via standalone on()', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
'todo:deleted': { id: string };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
const ee = createEmitter<EventHandlerPayloads>();
const todoDeletedHandler = defineHandler<EventHandlerPayloads, 'todo:deleted'>(
(_c, _payload: EventHandlerPayloads['todo:deleted']) => {},
);
const spy = vi.fn(todoDeletedHandler);
ee.on('todo:deleted', spy);
const app = new Hono<Env>();
let currentContext = null;
app.delete('/todo/123', (c) => {
currentContext = c;
ee.emit('todo:deleted', c, { id: '2' });
return c.json({ message: 'Todo created' });
});
const res = await app.request('http://localhost/todo/123', { method: 'DELETE' });
expect(res).not.toBeNull();
expect(res.status).toBe(200);
expect(spy).toHaveBeenCalledWith(currentContext, { id: '2' });
});
it('Should work removing event handlers via off() method', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
'todo:deleted': { id: string };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
const ee = createEmitter<EventHandlerPayloads>();
const todoDeletedHandler = defineHandler<EventHandlerPayloads, 'todo:deleted'>(
(_c, _payload: EventHandlerPayloads['todo:deleted']) => {},
);
const spy = vi.fn(todoDeletedHandler);
ee.on('todo:deleted', spy);
const app = new Hono<Env>();
app.post('/todo', (c) => {
ee.emit('todo:deleted', c, { id: '2' });
ee.off('todo:deleted', spy);
return c.json({ message: 'Todo created' });
});
await app.request('http://localhost/todo', { method: 'POST' });
await app.request('http://localhost/todo', { method: 'POST' });
expect(spy).toHaveBeenCalledTimes(1);
});
it('Should work removing all event handlers via off() method not providing handler as second argument', async () => {
type EventHandlerPayloads = {
'todo:deleted': { id: string };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
const ee = createEmitter<EventHandlerPayloads>();
const todoDeletedHandler = defineHandler<EventHandlerPayloads, 'todo:deleted'>(
(_c, _payload: EventHandlerPayloads['todo:deleted']) => {},
);
const todoDeletedHandler2 = defineHandler<EventHandlerPayloads, 'todo:deleted'>(
(_c, _payload: EventHandlerPayloads['todo:deleted']) => {},
);
const spy = vi.fn(todoDeletedHandler);
const spy2 = vi.fn(todoDeletedHandler2);
ee.on('todo:deleted', spy);
ee.on('todo:deleted', spy2);
const app = new Hono<Env>();
app.post('/todo', (c) => {
ee.emit('todo:deleted', c, { id: '2' });
ee.off('todo:deleted');
return c.json({ message: 'Todo created' });
});
await app.request('http://localhost/todo', { method: 'POST' });
await app.request('http://localhost/todo', { method: 'POST' });
expect(spy).toHaveBeenCalledTimes(1);
expect(spy2).toHaveBeenCalledTimes(1);
});
});
});
const res = await app.request('http://localhost/todo', { method: 'POST' })
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(handlers['todo:created']?.[0]).toHaveBeenCalledWith(currentContext, {
id: '2',
text: 'Buy milk',
})
})
})
})

View File

@ -3,17 +3,28 @@
* Event Emitter Middleware for Hono.
*/
import type { Context, Env, MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory'
import type { Context, Env, MiddlewareHandler } from 'hono'
export type EventKey = string | symbol;
export type EventHandler<T, E extends Env = Env> = (c: Context<E>, payload: T) => void | Promise<void>;
export type EventHandlers<T> = { [K in keyof T]?: EventHandler<T[K]>[] };
export type EventKey = string | symbol
export type EventHandler<T, E extends Env = Env> = (
c: Context<E>,
payload: T
) => void | Promise<void>
export type EventHandlers<T> = { [K in keyof T]?: EventHandler<T[K]>[] }
export type EventPayloadMap = { [key: string]: unknown }
export type EmitAsyncOptions = { mode: 'concurrent' | 'sequencial' }
export type EventEmitterOptions = { maxHandlers?: number }
export interface Emitter<EventHandlerPayloads> {
on<Key extends keyof EventHandlerPayloads>(key: Key, handler: EventHandler<EventHandlerPayloads[Key]>): void;
off<Key extends keyof EventHandlerPayloads>(key: Key, handler?: EventHandler<EventHandlerPayloads[Key]>): void;
emit<Key extends keyof EventHandlerPayloads>(key: Key, c: Context, payload: EventHandlerPayloads[Key]): void;
export interface Emitter<EPMap extends EventPayloadMap> {
on<Key extends keyof EPMap>(key: Key, handler: EventHandler<EPMap[Key]>): void
off<Key extends keyof EPMap>(key: Key, handler?: EventHandler<EPMap[Key]>): void
emit<Key extends keyof EPMap>(c: Context, key: Key, payload: EPMap[Key]): void
emitAsync<Key extends keyof EPMap>(
c: Context,
key: Key,
payload: EPMap[Key],
options?: EmitAsyncOptions
): Promise<void>
}
/**
@ -21,25 +32,33 @@ export interface Emitter<EventHandlerPayloads> {
* @param {EventHandler} handler - The event handlers.
* @returns The event handler.
*/
export const defineHandler = <T, K extends keyof T, E extends Env = Env>(
handler: EventHandler<T[K], E>,
): EventHandler<T[K], E> => {
return handler;
};
export const defineHandler = <
EPMap extends EventPayloadMap,
Key extends keyof EPMap,
E extends Env = Env
>(
handler: EventHandler<EPMap[Key], E>
): EventHandler<EPMap[Key], E> => {
return handler
}
/**
* Function to define fully typed event handlers.
* @param {EventHandler[]} handlers - An object where each key is an event type and the value is an array of event handlers.
* @returns The event handlers.
*/
export const defineHandlers = <T, E extends Env = Env>(handlers: { [K in keyof T]?: EventHandler<T[K], E>[] }) => {
return handlers;
};
export const defineHandlers = <EPMap extends EventPayloadMap, E extends Env = Env>(handlers: {
[K in keyof EPMap]?: EventHandler<EPMap[K], E>[]
}): { [K in keyof EPMap]?: EventHandler<EPMap[K], E>[] } => {
return handlers
}
/**
* Create Event Emitter instance.
*
* @param {EventHandlers} eventHandlers - Event handlers to be registered.
* @template EPMap - The event payload map.
* @param {EventHandlers<EPMap>} [eventHandlers] - Event handlers to be registered.
* @param {EventEmitterOptions} [options] - Options for the event emitter.
* @returns {Emitter} The EventEmitter instance.
*
* @example
@ -59,9 +78,14 @@ export const defineHandlers = <T, E extends Env = Env>(handlers: { [K in keyof T
* c.get('logger').log('Bar:', payload.item.id)
* })
*
* ee.on('baz', async (c, payload) => {
* // Do something async
* })
*
* // Use the emitter to emit events.
* ee.emit('foo', c, 42)
* ee.emit('bar', c, { item: { id: '12345678' } })
* ee.emit(c, 'foo', 42)
* ee.emit(c, 'bar', { item: { id: '12345678' } })
* await ee.emitAsync(c, 'baz', { item: { id: '12345678' } })
* ```
*
* ```ts
@ -69,6 +93,7 @@ export const defineHandlers = <T, E extends Env = Env>(handlers: { [K in keyof T
* // event key: payload type
* 'foo': number;
* 'bar': { item: { id: string } };
* 'baz': { item: { id: string } };
* };
*
* // Define event handlers
@ -86,33 +111,52 @@ export const defineHandlers = <T, E extends Env = Env>(handlers: { [K in keyof T
* c.get('logger').log('Bar:', payload.item.id)
* })
*
* ee.on('baz', async (c, payload) => {
* // Do something async
* })
*
* // Use the emitter to emit events.
* ee.emit('foo', c, 42) // Payload will be expected to be of a type number
* ee.emit('bar', c, { item: { id: '12345678' }, c }) // Payload will be expected to be of a type { item: { id: string }, c: Context }
* ee.emit(c, 'foo', 42) // Payload will be expected to be of a type number
* ee.emit(c, 'bar', { item: { id: '12345678' } }) // Payload will be expected to be of a type { item: { id: string }, c: Context }
* await ee.emitAsync(c, 'baz', { item: { id: '12345678' } }) // Payload will be expected to be of a type { item: { id: string } }
* ```
*
*/
export const createEmitter = <EventHandlerPayloads>(
eventHandlers?: EventHandlers<EventHandlerPayloads>,
): Emitter<EventHandlerPayloads> => {
export const createEmitter = <EPMap extends EventPayloadMap>(
eventHandlers?: EventHandlers<EPMap>,
options?: EventEmitterOptions
): Emitter<EPMap> => {
// A map of event keys and their corresponding event handlers.
const handlers: Map<EventKey, EventHandler<unknown>[]> = eventHandlers
? new Map(Object.entries(eventHandlers))
: new Map();
: new Map()
return {
/**
* Add an event handler for the given event key.
* @param {string|symbol} key Type of event to listen for
* @param {Function} handler Function that is invoked when the specified event occurs
* @throws {TypeError} If the handler is not a function
*/
on<Key extends keyof EventHandlerPayloads>(key: Key, handler: EventHandler<EventHandlerPayloads[Key]>) {
if (!handlers.has(key as EventKey)) {
handlers.set(key as EventKey, []);
on<Key extends keyof EPMap>(key: Key, handler: EventHandler<EPMap[Key]>) {
if (typeof handler !== 'function') {
throw new TypeError('The handler must be a function')
}
if (!handlers.has(key as EventKey)) {
handlers.set(key as EventKey, [])
}
const handlerArray = handlers.get(key as EventKey) as Array<EventHandler<EPMap[Key]>>
const limit = options?.maxHandlers ?? 10
if (handlerArray.length >= limit) {
throw new RangeError(
`Max handlers limit (${limit}) reached for the event "${String(key)}".
This may indicate a memory leak,
perhaps due to adding anonymous function as handler within middleware or request handler.
Check your code or consider increasing limit using options.maxHandlers.`
)
}
const handlerArray = handlers.get(key as EventKey) as Array<EventHandler<EventHandlerPayloads[Key]>>;
if (!handlerArray.includes(handler)) {
handlerArray.push(handler);
handlerArray.push(handler)
}
},
@ -122,16 +166,16 @@ export const createEmitter = <EventHandlerPayloads>(
* @param {string|symbol} key Type of event to unregister `handler` from
* @param {Function} handler - Handler function to remove
*/
off<Key extends keyof EventHandlerPayloads>(key: Key, handler?: EventHandler<EventHandlerPayloads[Key]>) {
off<Key extends keyof EPMap>(key: Key, handler?: EventHandler<EPMap[Key]>) {
if (!handler) {
handlers.delete(key as EventKey);
handlers.delete(key as EventKey)
} else {
const handlerArray = handlers.get(key as EventKey);
const handlerArray = handlers.get(key as EventKey)
if (handlerArray) {
handlers.set(
key as EventKey,
handlerArray.filter((h) => h !== handler),
);
handlerArray.filter((h) => h !== handler)
)
}
}
},
@ -139,27 +183,69 @@ export const createEmitter = <EventHandlerPayloads>(
/**
* Emit an event with the given event key and payload.
* Triggers all event handlers associated with the specified key.
* @param {string|symbol} key - The event key
* @param {Context} c - The current context object
* @param {EventHandlerPayloads[keyof EventHandlerPayloads]} payload - Data passed to each invoked handler
* @param {string|symbol} key - The event key
* @param {EventPayloadMap[keyof EventPayloadMap]} payload - Data passed to each invoked handler
*/
emit<Key extends keyof EventHandlerPayloads>(key: Key, c: Context, payload: EventHandlerPayloads[Key]) {
const handlerArray = handlers.get(key as EventKey);
emit<Key extends keyof EPMap>(c: Context, key: Key, payload: EPMap[Key]) {
const handlerArray = handlers.get(key as EventKey)
if (handlerArray) {
for (const handler of handlerArray) {
handler(c, payload);
handler(c, payload)
}
}
},
};
};
/**
* Emit an event with the given event key and payload.
* Asynchronously triggers all event handlers associated with the specified key.
* @param {Context} c - The current context object
* @param {string|symbol} key - The event key
* @param {EventPayloadMap[keyof EventPayloadMap]} payload - Data passed to each invoked handler
* @param {EmitAsyncOptions} options - Options.
* @throws {AggregateError} If any handler encounters an error.
*/
async emitAsync<Key extends keyof EPMap>(
c: Context,
key: Key,
payload: EPMap[Key],
options: EmitAsyncOptions = { mode: 'concurrent' }
) {
const handlerArray = handlers.get(key as EventKey)
if (handlerArray) {
if (options.mode === 'sequencial') {
for (const handler of handlerArray) {
await handler(c, payload)
}
} else {
const results = await Promise.allSettled(
handlerArray.map(async (handler) => {
await handler(c, payload)
})
)
const errors = (
results.filter((r) => r.status === 'rejected') as PromiseRejectedResult[]
).map((e) => e.reason)
if (errors.length > 0) {
throw new AggregateError(
errors,
`${errors.length} handler(s) for event ${String(key)} encountered errors`
)
}
}
}
},
}
}
/**
* Event Emitter Middleware for Hono.
*
* @see {@link https://hono.dev/middleware/builtin/event-emitter}
* @see {@link https://github.com/honojs/middleware/tree/main/packages/event-emitter}
*
* @param {EventHandlers} eventHandlers - Event handlers to be registered.
* @template EPMap - The event payload map.
* @param {EventHandlers<EPMap>} [eventHandlers] - Event handlers to be registered.
* @param {EventEmitterOptions} [options] - Options for the event emitter.
* @returns {MiddlewareHandler} The middleware handler function.
*
* @example
@ -169,9 +255,14 @@ export const createEmitter = <EventHandlerPayloads>(
* const handlers: {
* 'foo': [
* (c, payload) => { console.log('Foo:', payload) }
* ]
* ],
* 'bar': [
* (c, payload) => { console.log('Bar:', payload.item.id) }
* ],
* 'baz': [
* async (c, payload) => {
* // Do something async
* }
* ]
* }
*
@ -183,8 +274,9 @@ export const createEmitter = <EventHandlerPayloads>(
* // Use the emitter in route handlers to emit events.
* app.post('/foo', async (c) => {
* // The emitter is available under "emitter" key in the context.
* c.get('emitter').emit('foo', c, 42)
* c.get('emitter').emit('bar', c, { item: { id: '12345678' } })
* c.get('emitter').emit(c, 'foo', 42)
* c.get('emitter').emit(c, 'bar', { item: { id: '12345678' } })
* await c.get('emitter').emitAsync(c, 'baz', { item: { id: '12345678' } })
* return c.text('Success')
* })
* ```
@ -194,6 +286,7 @@ export const createEmitter = <EventHandlerPayloads>(
* // event key: payload type
* 'foo': number;
* 'bar': { item: { id: string } };
* 'baz': { item: { id: string } };
* };
*
* type Env = { Bindings: {}; Variables: { emitter: Emitter<AvailableEvents> }; }
@ -202,9 +295,14 @@ export const createEmitter = <EventHandlerPayloads>(
* const handlers: defineHandlers<AvailableEvents>({
* 'foo': [
* (c, payload) => { console.log('Foo:', payload) } // payload will be inferred as number
* ]
* ],
* 'bar': [
* (c, payload) => { console.log('Bar:', payload.item.id) } // payload will be inferred as { item: { id: string } }
* ],
* 'baz': [
* async (c, payload) => {
* // Do something async
* }
* ]
* })
*
@ -216,19 +314,21 @@ export const createEmitter = <EventHandlerPayloads>(
* // Use the emitter in route handlers to emit events.
* app.post('/foo', async (c) => {
* // The emitter is available under "emitter" key in the context.
* c.get('emitter').emit('foo', c, 42) // Payload will be expected to be of a type number
* c.get('emitter').emit('bar', c, { item: { id: '12345678' } }) // Payload will be expected to be of a type { item: { id: string } }
* c.get('emitter').emit(c, 'foo', 42) // Payload will be expected to be of a type number
* c.get('emitter').emit(c, 'bar', { item: { id: '12345678' } }) // Payload will be expected to be of a type { item: { id: string } }
* await c.get('emitter').emitAsync(c, 'baz', { item: { id: '12345678' } }) // Payload will be expected to be of a type { item: { id: string } }
* return c.text('Success')
* })
* ```
*/
export const emitter = <EventHandlerPayloads>(
eventHandlers?: EventHandlers<EventHandlerPayloads>,
export const emitter = <EPMap extends EventPayloadMap>(
eventHandlers?: EventHandlers<EPMap>,
options?: EventEmitterOptions
): MiddlewareHandler => {
// Create new instance to share with any middleware and handlers
const instance = createEmitter<EventHandlerPayloads>(eventHandlers);
return createMiddleware(async (c, next) => {
c.set('emitter', instance);
await next();
});
};
const instance = createEmitter<EPMap>(eventHandlers, options)
return async (c, next) => {
c.set('emitter', instance)
await next()
}
}

View File

@ -2299,9 +2299,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hono/event-emitter@workspace:packages/event-emitter"
dependencies:
hono: "npm:^3.11.7"
hono: "npm:^4.3.6"
tsup: "npm:^8.0.1"
vitest: "npm:^1.0.4"
vitest: "npm:^1.6.0"
peerDependencies:
hono: "*"
languageName: unknown