honojs-middleware/packages/event-emitter/src/index.ts

335 lines
11 KiB
TypeScript

/**
* @module
* Event Emitter Middleware for Hono.
*/
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 EventPayloadMap = { [key: string]: unknown }
export type EmitAsyncOptions = { mode: 'concurrent' | 'sequencial' }
export type EventEmitterOptions = { maxHandlers?: number }
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>
}
/**
* Function to define fully typed event handler.
* @param {EventHandler} handler - The event handlers.
* @returns The event 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 = <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.
*
* @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
* ```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)
* })
*
* ee.on('baz', async (c, payload) => {
* // Do something async
* })
*
* // Use the emitter to emit events.
* ee.emit(c, 'foo', 42)
* ee.emit(c, 'bar', { item: { id: '12345678' } })
* await ee.emitAsync(c, 'baz', { item: { id: '12345678' } })
* ```
*
* ```ts
* type AvailableEvents = {
* // event key: payload type
* 'foo': number;
* 'bar': { item: { id: string } };
* 'baz': { 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)
* })
*
* ee.on('baz', async (c, payload) => {
* // Do something async
* })
*
* // Use the emitter to emit events.
* 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 = <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()
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 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 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.`
)
}
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 EPMap>(key: Key, handler?: EventHandler<EPMap[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 {Context} c - The current context object
* @param {string|symbol} key - The event key
* @param {EventPayloadMap[keyof EventPayloadMap]} payload - Data passed to each invoked handler
*/
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)
}
}
},
/**
* 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://github.com/honojs/middleware/tree/main/packages/event-emitter}
*
* @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
* ```js
*
* // Define event handlers
* 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
* }
* ]
* }
*
* 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(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')
* })
* ```
*
* ```ts
* type AvailableEvents = {
* // event key: payload type
* 'foo': number;
* 'bar': { item: { id: string } };
* 'baz': { 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 } }
* ],
* 'baz': [
* async (c, payload) => {
* // Do something async
* }
* ]
* })
*
* 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(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 = <EPMap extends EventPayloadMap>(
eventHandlers?: EventHandlers<EPMap>,
options?: EventEmitterOptions
): MiddlewareHandler => {
// Create new instance to share with any middleware and handlers
const instance = createEmitter<EPMap>(eventHandlers, options)
return async (c, next) => {
c.set('emitter', instance)
await next()
}
}