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('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(handlers) expect(ee).toBeDefined() expect(ee.emit).toBeDefined() expect(ee.on).toBeDefined() expect(ee.off).toBeDefined() expect(ee.emitAsync).toBeDefined() }) it('should create an emitter without initial handlers', () => { const ee = createEmitter() expect(ee).toBeDefined() }) it('should allow adding and removing handlers', () => { type EventPayloadMap = { test: string } const ee = createEmitter() const handler = vi.fn() ee.on('test', handler) ee.emit({} as Context, 'test', 'payload') expect(handler).toHaveBeenCalledWith({}, 'payload') ee.off('test', handler) ee.emit({} as Context, 'test', 'payload') expect(handler).toHaveBeenCalledTimes(1) }) it('should remove all handlers for an event when no handler is specified', () => { type EventPayloadMap = { test: string } const ee = createEmitter() 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() }) it('should emit events to all registered handlers', () => { type EventPayloadMap = { test: string } const ee = createEmitter() 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') }) it('should not add the same named function handler multiple times', () => { type EventPayloadMap = { test: string } const ee = createEmitter() const handler = vi.fn() ee.on('test', handler) ee.on('test', handler) ee.emit({} as Context, 'test', 'payload') expect(handler).toHaveBeenCalledTimes(1) }) it('should emit async events concurrently', async () => { type EventPayloadMap = { test: { id: string } } const ee = createEmitter() const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const handler1 = vi.fn( defineHandler(async (_c, _payload) => { await delay(100) }) ) const handler2 = vi.fn( defineHandler(async (_c, _payload) => { await delay(100) }) ) ee.on('test', handler1) ee.on('test', handler2) const start = Date.now() await ee.emitAsync({} as Context, 'test', { id: '123' }, { mode: 'concurrent' }) const end = Date.now() // 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) expect(handler1).toHaveBeenCalledWith(expect.anything(), { id: '123' }) expect(handler2).toHaveBeenCalledWith(expect.anything(), { id: '123' }) expect(handler1).toHaveBeenCalledTimes(1) expect(handler2).toHaveBeenCalledTimes(1) }) it('should emit async events sequentially', async () => { type EventPayloadMap = { test: { id: string } } const ee = createEmitter() const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const handler1 = vi.fn( defineHandler(async (_c, _payload) => { await delay(101) }) ) const handler2 = vi.fn( defineHandler(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() // 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) expect(handler1).toHaveBeenCalledWith(expect.anything(), { id: '123' }) expect(handler2).toHaveBeenCalledWith(expect.anything(), { id: '123' }) expect(handler1).toHaveBeenCalledTimes(1) expect(handler2).toHaveBeenCalledTimes(1) }) it('should throw AggregateError when async handlers fail using emitAsync with concurent mode', async () => { type EventPayloadMap = { test: string } const ee = createEmitter() 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 stop execution on first error in async handlers fail using emitAsync with sequential mode', async () => { type EventPayloadMap = { test: { id: string } } const ee = createEmitter() const handler1 = vi.fn( defineHandler(async () => { throw new Error('Error 1') }) ) const handler2 = vi.fn( defineHandler(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() // biome-ignore lint/suspicious/noExplicitAny: 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({}, { 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() 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({}, { 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({}, { 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({}, { 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() 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() 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() 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({ test: [handler] }) let capturedEmitter: Emitter | 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(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() // 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 } } const handlers = defineHandlers({ 'todo:created': [vi.fn((_c, _payload) => {})], }) const app = new Hono() 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(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', }) }) it('should work when assigning async event handlers via middleware', async () => { type EventPayloadMap = { 'todo:created': { id: string; text: string } } type Env = { Variables: { emitter: Emitter } } const handlers = defineHandlers({ 'todo:created': [vi.fn(async (_c, _payload) => {})], }) const app = new Hono() app.use(emitter(handlers)) 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 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', }) }) }) })