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 versionpull/625/head
parent
9bf6c4bb8e
commit
53b4f33190
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/event-emitter': major
|
||||
---
|
||||
|
||||
Full release of Event Emitter middleware for Hono
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
},
|
||||
})
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue