feat: Event Emitter middleware (#615)

* Add event-emitter middleware

* Commit yarn.lock

* Add author to package.json

* Remove CHANGELOG.md

* Bump up node version

* Adjust initial version
pull/625/head
David Havl 2024-07-07 09:03:10 +02:00 committed by GitHub
parent 9bf6c4bb8e
commit 53b4f33190
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 940 additions and 0 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/event-emitter': major
---
Full release of Event Emitter middleware for Hono

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -0,0 +1,43 @@
{
"name": "@hono/event-emitter",
"version": "0.0.0",
"description": "Event Emitter middleware for Hono",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "vitest --run",
"build": "tsup ./src/index.ts --format esm,cjs --dts",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/honojs/middleware.git"
},
"homepage": "https://github.com/honojs/middleware",
"author": "David Havl <contact@davidhavl.com> (https://github.com/DavidHavl)",
"peerDependencies": {
"hono": "*"
},
"devDependencies": {
"hono": "^3.11.7",
"tsup": "^8.0.1",
"vitest": "^1.0.4"
}
}

View File

@ -0,0 +1,243 @@
import { Hono } from 'hono';
import { expect, vi } from 'vitest';
import { emitter, createEmitter, type Emitter, type EventHandlers, defineHandler, defineHandlers } from '../src';
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> } };
const handler = defineHandler<EventHandlerPayloads, 'todo:created'>((_c, _payload) => {});
const spy = vi.fn(handler);
const app = new Hono<Env>();
app.use('*', emitter());
app.use((c, next) => {
c.get('emitter').on('todo:created', spy);
return next();
});
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' });
});
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 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> } };
const handler = defineHandler<EventHandlerPayloads, 'todo:created'>((_c, _payload) => {});
const spy = vi.fn(handler);
const app = new Hono<Env>();
app.use('*', emitter());
app.use((c, next) => {
c.get('emitter').on('todo:created', spy);
return next();
});
app.post('/todo', (c) => {
c.get('emitter').emit('todo:created', c, { id: '2', text: 'Buy milk' });
return c.json({ message: 'Todo created' });
});
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 work assigning event handlers via middleware', async () => {
type EventHandlerPayloads = {
'todo:created': { id: string; text: string };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
const handlers = defineHandlers<EventHandlerPayloads>({
'todo:created': [vi.fn((_c, _payload) => {})],
});
const app = new Hono<Env>();
app.use('*', emitter(handlers));
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' });
});
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 };
};
type Env = { Variables: { emitter: Emitter<EventHandlerPayloads> } };
const handlers: EventHandlers<EventHandlerPayloads> = {
'todo:created': [vi.fn((_payload) => {})],
};
const ee = createEmitter<EventHandlerPayloads>(handlers);
const todoDeletedHandler = vi.fn(defineHandler<EventHandlerPayloads, 'todo:deleted'>((_c, _payload) => {}));
ee.on('todo:deleted', todoDeletedHandler);
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);
});
});
});

View File

@ -0,0 +1,234 @@
/**
* @module
* Event Emitter Middleware for Hono.
*/
import type { Context, Env, MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory'
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 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;
}
/**
* Function to define fully typed event handler.
* @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;
};
/**
* 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;
};
/**
* Create Event Emitter instance.
*
* @param {EventHandlers} eventHandlers - Event handlers to be registered.
* @returns {Emitter} The EventEmitter instance.
*
* @example
* ```js
* // Define event handlers
* const handlers: {
* 'foo': [
* (c, payload) => { console.log('Foo:', payload) }
* ]
* }
*
* // Initialize emitter with handlers
* const ee = createEmitter(handlers)
*
* // AND/OR add more listeners on the fly.
* ee.on('bar', (c, payload) => {
* c.get('logger').log('Bar:', payload.item.id)
* })
*
* // Use the emitter to emit events.
* ee.emit('foo', c, 42)
* ee.emit('bar', c, { item: { id: '12345678' } })
* ```
*
* ```ts
* type AvailableEvents = {
* // event key: payload type
* 'foo': number;
* 'bar': { item: { id: string } };
* };
*
* // Define event handlers
* const handlers: defineHandlers<AvailableEvents>({
* 'foo': [
* (c, payload) => { console.log('Foo:', payload) } // payload will be inferred as number
* ]
* })
*
* // Initialize emitter with handlers
* const ee = createEmitter(handlers)
*
* // AND/OR add more listeners on the fly.
* ee.on('bar', (c, payload) => {
* c.get('logger').log('Bar:', payload.item.id)
* })
*
* // 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 }
* ```
*
*/
export const createEmitter = <EventHandlerPayloads>(
eventHandlers?: EventHandlers<EventHandlerPayloads>,
): Emitter<EventHandlerPayloads> => {
// A map of event keys and their corresponding event handlers.
const handlers: Map<EventKey, EventHandler<unknown>[]> = eventHandlers
? new Map(Object.entries(eventHandlers))
: 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
*/
on<Key extends keyof EventHandlerPayloads>(key: Key, handler: EventHandler<EventHandlerPayloads[Key]>) {
if (!handlers.has(key as EventKey)) {
handlers.set(key as EventKey, []);
}
const handlerArray = handlers.get(key as EventKey) as Array<EventHandler<EventHandlerPayloads[Key]>>;
if (!handlerArray.includes(handler)) {
handlerArray.push(handler);
}
},
/**
* Remove an event handler for the given event key.
* If `handler` is undefined, all handlers for the given key are removed.
* @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]>) {
if (!handler) {
handlers.delete(key as EventKey);
} else {
const handlerArray = handlers.get(key as EventKey);
if (handlerArray) {
handlers.set(
key as EventKey,
handlerArray.filter((h) => h !== handler),
);
}
}
},
/**
* 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
*/
emit<Key extends keyof EventHandlerPayloads>(key: Key, c: Context, payload: EventHandlerPayloads[Key]) {
const handlerArray = handlers.get(key as EventKey);
if (handlerArray) {
for (const handler of handlerArray) {
handler(c, payload);
}
}
},
};
};
/**
* Event Emitter Middleware for Hono.
*
* @see {@link https://hono.dev/middleware/builtin/event-emitter}
*
* @param {EventHandlers} eventHandlers - Event handlers to be registered.
* @returns {MiddlewareHandler} The middleware handler function.
*
* @example
* ```js
*
* // Define event handlers
* const handlers: {
* 'foo': [
* (c, payload) => { console.log('Foo:', payload) }
* ]
* 'bar': [
* (c, payload) => { console.log('Bar:', payload.item.id) }
* ]
* }
*
* const app = new Hono()
*
* // Register the emitter middleware and provide it with the handlers
* app.use('\*', emitter(handlers))
*
* // 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' } })
* return c.text('Success')
* })
* ```
*
* ```ts
* type AvailableEvents = {
* // event key: payload type
* 'foo': number;
* 'bar': { item: { id: string } };
* };
*
* type Env = { Bindings: {}; Variables: { emitter: Emitter<AvailableEvents> }; }
*
* // Define event handlers
* 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 } }
* ]
* })
*
* const app = new Hono<Env>()
*
* // Register the emitter middleware and provide it with the handlers
* app.use('\*', emitter(handlers))
*
* // 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 } }
* return c.text('Success')
* })
* ```
*/
export const emitter = <EventHandlerPayloads>(
eventHandlers?: EventHandlers<EventHandlerPayloads>,
): 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();
});
};

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"include": [
"src/**/*.ts"
],
}

View File

@ -0,0 +1,8 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
},
})

View File

@ -1880,6 +1880,18 @@ __metadata:
languageName: unknown
linkType: soft
"@hono/event-emitter@workspace:packages/event-emitter":
version: 0.0.0-use.local
resolution: "@hono/event-emitter@workspace:packages/event-emitter"
dependencies:
hono: "npm:^3.11.7"
tsup: "npm:^8.0.1"
vitest: "npm:^1.0.4"
peerDependencies:
hono: "*"
languageName: unknown
linkType: soft
"@hono/firebase-auth@workspace:packages/firebase-auth":
version: 0.0.0-use.local
resolution: "@hono/firebase-auth@workspace:packages/firebase-auth"