335 lines
11 KiB
TypeScript
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()
|
|
}
|
|
}
|