fix(server): event handler bindings (#10165)

This commit is contained in:
forehalo
2025-02-14 11:29:02 +00:00
parent 42e0563d2e
commit 3dde47dd08
18 changed files with 486 additions and 260 deletions

View File

@@ -51,20 +51,32 @@ test('should broadcast event to cluster instances', async t => {
// app 2 for broadcasting
const eventbus2 = app2.get(EventBus);
const cls = ClsServiceManager.getClsService();
cls.run(() => {
cls.set(CLS_ID, 'test-request-id');
eventbus2.broadcast('__test__.event', { count: 0, requestId: cls.getId() });
});
eventbus2.broadcast('__test__.event', { count: 0 });
// cause the cross instances broadcasting is asynchronization calling
// we should wait for the event's arriving before asserting
await eventbus1.waitFor('__test__.event');
t.true(listener.calledOnceWith({ count: 0, requestId: 'test-request-id' }));
t.true(
runtimeListener.calledOnceWith({ count: 0, requestId: 'test-request-id' })
);
t.true(listener.calledOnceWith({ count: 0 }));
t.true(runtimeListener.calledOnceWith({ count: 0 }));
off();
});
test('should continuously use the same request id', async t => {
const { app1, app2 } = t.context;
const eventbus1 = app1.get(EventBus);
const eventbus2 = app2.get(EventBus);
const listener = Sinon.spy(app1.get(Listeners), 'onRequestId');
const cls = ClsServiceManager.getClsService();
cls.run(() => {
cls.set(CLS_ID, 'test-request-id');
eventbus2.broadcast('__test__.requestId', {});
});
await eventbus1.waitFor('__test__.requestId');
t.true(listener.lastCall.returned('test-request-id'));
});

View File

@@ -1,5 +1,6 @@
import { TestingModule } from '@nestjs/testing';
import ava, { TestFn } from 'ava';
import { CLS_ID, ClsServiceManager } from 'nestjs-cls';
import Sinon from 'sinon';
import { EventBus, metrics } from '../../base';
@@ -9,7 +10,7 @@ import { Listeners } from './provider';
export const test = ava as TestFn<{
module: TestingModule;
eventbus: EventBus;
listener: Sinon.SinonSpy;
listeners: Sinon.SinonSpiedInstance<Listeners>;
}>;
test.before(async t => {
@@ -19,30 +20,20 @@ test.before(async t => {
const eventbus = m.get(EventBus);
t.context.module = m;
t.context.eventbus = eventbus;
t.context.listener = Sinon.spy(m.get(Listeners), 'onTestEvent');
});
test.afterEach(() => {
Sinon.reset();
test.beforeEach(t => {
Sinon.restore();
const { module } = t.context;
t.context.listeners = Sinon.spy(module.get(Listeners));
});
test.after(async t => {
await t.context.module.close();
});
test('should register event listener', t => {
const { eventbus } = t.context;
// @ts-expect-error private member
t.true(eventbus.emitter.eventNames().includes('__test__.event'));
eventbus.on('__test__.event2', () => {});
// @ts-expect-error private member
t.true(eventbus.emitter.eventNames().includes('__test__.event2'));
});
test('should dispatch event listener', t => {
const { eventbus, listener } = t.context;
const { eventbus, listeners } = t.context;
const runtimeListener = Sinon.stub();
const off = eventbus.on('__test__.event', runtimeListener);
@@ -50,29 +41,53 @@ test('should dispatch event listener', t => {
const payload = { count: 0 };
eventbus.emit('__test__.event', payload);
t.true(listener.calledOnceWithExactly(payload));
t.true(listeners.onTestEvent.calledOnceWithExactly(payload));
t.true(runtimeListener.calledOnceWithExactly(payload));
off();
});
test('should dispatch async event listener', async t => {
const { eventbus, listener } = t.context;
const { eventbus, listeners } = t.context;
const runtimeListener = Sinon.stub().returns({ count: 2 });
const runtimeListener = Sinon.stub().returnsArg(0);
const off = eventbus.on('__test__.event', runtimeListener);
const payload = { count: 0 };
const returns = await eventbus.emitAsync('__test__.event', payload);
t.true(listener.calledOnceWithExactly(payload));
t.true(listeners.onTestEvent.calledOnceWithExactly(payload));
t.true(listeners.onTestEventAndEvent2.calledOnceWithExactly(payload));
t.true(runtimeListener.calledOnceWithExactly(payload));
t.deepEqual(returns, [{ count: 1 }, { count: 2 }]);
t.deepEqual(returns, [payload, payload, payload]);
off();
});
test('should dispatch multiple event handlers with same name', async t => {
const { eventbus, listeners } = t.context;
const payload = { count: 0 };
await eventbus.emitAsync('__test__.event', payload);
t.true(listeners.onTestEvent.calledOnceWithExactly(payload));
t.true(listeners.onTestEventAndEvent2.calledOnceWithExactly(payload));
});
test('should dispatch event listener with multiple event names', async t => {
const { eventbus, listeners } = t.context;
const payload = { count: 0 };
await eventbus.emitAsync('__test__.event', payload);
t.like(listeners.onTestEventAndEvent2.lastCall.args[0], payload);
await eventbus.emitAsync('__test__.event2', payload);
t.like(listeners.onTestEventAndEvent2.lastCall.args[0], payload);
});
test('should record event handler call metrics', async t => {
const { eventbus } = t.context;
const timerStub = Sinon.stub(
@@ -86,26 +101,103 @@ test('should record event handler call metrics', async t => {
await eventbus.emitAsync('__test__.event', { count: 0 });
t.deepEqual(timerStub.getCall(0).args[1], {
t.true(timerStub.calledTwice);
t.deepEqual(timerStub.firstCall.args[1], {
name: 'event_handler',
event: '__test__.event',
namespace: '__test__',
handler: 'Listeners.onTestEvent',
error: false,
});
t.deepEqual(timerStub.lastCall.args[1], {
name: 'event_handler',
event: '__test__.event',
namespace: '__test__',
handler: 'Listeners.onTestEventAndEvent2',
error: false,
});
t.deepEqual(counterStub.getCall(0).args[1], {
t.true(counterStub.calledTwice);
t.deepEqual(counterStub.firstCall.args[1], {
name: 'event_handler',
event: '__test__.event',
namespace: '__test__',
handler: 'Listeners.onTestEvent',
error: false,
});
t.deepEqual(counterStub.lastCall.args[1], {
name: 'event_handler',
event: '__test__.event',
namespace: '__test__',
handler: 'Listeners.onTestEventAndEvent2',
error: false,
});
Sinon.reset();
timerStub.reset();
counterStub.reset();
await eventbus.emitAsync('__test__.event2', { count: 0 });
await eventbus.emitAsync('__test__.throw', { count: 0 });
t.true(timerStub.calledOnce);
t.deepEqual(timerStub.firstCall.args[1], {
name: 'event_handler',
event: '__test__.event2',
namespace: '__test__',
handler: 'Listeners.onTestEventAndEvent2',
error: false,
});
t.deepEqual(timerStub.getCall(0).args[1], {
t.true(counterStub.calledOnce);
t.deepEqual(counterStub.firstCall.args[1], {
name: 'event_handler',
event: '__test__.event2',
namespace: '__test__',
handler: 'Listeners.onTestEventAndEvent2',
error: false,
});
timerStub.reset();
counterStub.reset();
try {
await eventbus.emitAsync('__test__.throw', { count: 0 });
} catch {
// noop
}
t.true(timerStub.calledOnce);
t.deepEqual(timerStub.firstCall.args[1], {
name: 'event_handler',
event: '__test__.throw',
namespace: '__test__',
handler: 'Listeners.onThrow',
error: true,
});
t.true(counterStub.calledOnce);
t.deepEqual(counterStub.firstCall.args[1], {
name: 'event_handler',
event: '__test__.throw',
namespace: '__test__',
handler: 'Listeners.onThrow',
error: true,
});
});
test('should generate request id for event', async t => {
const { eventbus, listeners } = t.context;
await eventbus.emitAsync('__test__.requestId', {});
t.true(listeners.onRequestId.lastCall.returnValue.includes(':event/'));
});
test('should continuously use the same request id', async t => {
const { eventbus, listeners } = t.context;
const cls = ClsServiceManager.getClsService();
await cls.run(async () => {
cls.set(CLS_ID, 'test-request-id');
await eventbus.emitAsync('__test__.requestId', {});
});
t.true(listeners.onRequestId.lastCall.returned('test-request-id'));
});

View File

@@ -1,31 +1,40 @@
import { Injectable } from '@nestjs/common';
import { ClsServiceManager } from 'nestjs-cls';
import { OnEvent } from '../../base';
import { genRequestId, OnEvent } from '../../base';
declare global {
interface Events {
'__test__.event': { count: number; requestId?: string };
'__test__.event': { count: number };
'__test__.event2': { count: number };
'__test__.throw': { count: number };
'__test__.requestId': {};
}
}
@Injectable()
export class Listeners {
@OnEvent('__test__.event')
onTestEvent({ count, requestId }: Events['__test__.event']) {
return requestId
? {
count: count + 1,
requestId,
}
: {
count: count + 1,
};
onTestEvent(payload: Events['__test__.event']) {
return payload;
}
@OnEvent('__test__.event')
@OnEvent('__test__.event2')
onTestEventAndEvent2(
payload: Events['__test__.event'] | Events['__test__.event2']
) {
return payload;
}
@OnEvent('__test__.throw')
onThrow() {
throw new Error('Error in event handler');
}
@OnEvent('__test__.requestId')
onRequestId() {
const cls = ClsServiceManager.getClsService();
return cls.getId() ?? genRequestId('event');
}
}