chore(event-bus-*): Do not emit if no subscribers: (#14084)

* chore(event-vus-*): Do not emit if no subscribers:

* Create curly-apricots-double.md

* add tests

* align tests
This commit is contained in:
Adrien de Peretti
2025-11-24 09:36:52 +01:00
committed by GitHub
parent 6c3ec528f1
commit 113f200a99
6 changed files with 373 additions and 23 deletions
@@ -26,13 +26,21 @@ describe("LocalEventBusService", () => {
})
it("should emit an event", async () => {
eventBus.subscribe("eventName", () => Promise.resolve())
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn((event) =>
event === "eventName" ? 1 : 0
)
await eventBus.emit({
name: "eventName",
data: { hi: "1234" },
})
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(eventEmitter.emit).toHaveBeenCalledTimes(1)
expect(eventEmitter.emit).toHaveBeenCalledWith("eventName", {
data: { hi: "1234" },
@@ -41,12 +49,17 @@ describe("LocalEventBusService", () => {
expect(loggerMock.info).toHaveBeenCalledTimes(1)
expect(loggerMock.info).toHaveBeenCalledWith(
"Processing eventName which has undefined subscribers"
"Processing eventName which has 1 subscribers"
)
})
it("should emit an event but not log anything if it is internal", async () => {
eventBus.subscribe("eventName", () => Promise.resolve())
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn((event) =>
event === "eventName" ? 1 : 0
)
await eventBus.emit({
name: "eventName",
@@ -56,6 +69,9 @@ describe("LocalEventBusService", () => {
},
})
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(eventEmitter.emit).toHaveBeenCalledTimes(1)
expect(eventEmitter.emit).toHaveBeenCalledWith("eventName", {
data: { hi: "1234" },
@@ -74,6 +90,9 @@ describe("LocalEventBusService", () => {
}
)
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(eventEmitter.emit).toHaveBeenCalledTimes(2)
expect(eventEmitter.emit).toHaveBeenCalledWith("eventName", {
data: { hi: "1234" },
@@ -84,13 +103,22 @@ describe("LocalEventBusService", () => {
})
it("should emit multiple events", async () => {
eventBus.subscribe("event-1", () => Promise.resolve())
eventBus.subscribe("event-2", () => Promise.resolve())
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn((event) =>
event === "event-1" || event === "event-2" ? 1 : 0
)
await eventBus.emit([
{ name: "event-1", data: { hi: "1234" } },
{ name: "event-2", data: { hi: "5678" } },
])
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(eventEmitter.emit).toHaveBeenCalledTimes(2)
expect(eventEmitter.emit).toHaveBeenCalledWith("event-1", {
data: { hi: "1234" },
@@ -161,7 +189,13 @@ describe("LocalEventBusService", () => {
})
it("should release events when requested with eventGroupId", async () => {
eventBus.subscribe("event-1", () => Promise.resolve())
eventBus.subscribe("event-2", () => Promise.resolve())
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn((event) =>
event === "event-1" || event === "event-2" ? 1 : 0
)
await eventBus.emit([
{
@@ -187,6 +221,9 @@ describe("LocalEventBusService", () => {
{ name: "event-1", data: { test: "1" } },
])
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(eventEmitter.emit).toHaveBeenCalledTimes(1)
expect(eventEmitter.emit).toHaveBeenCalledWith("event-1", {
data: { test: "1" },
@@ -204,6 +241,9 @@ describe("LocalEventBusService", () => {
eventEmitter.emit = jest.fn((data) => data)
await eventBus.releaseGroupedEvents("group-1")
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(
(eventBus as any).groupedEventsMap_.get("group-1")
).not.toBeDefined()
@@ -254,5 +294,139 @@ describe("LocalEventBusService", () => {
expect(getMap().get("group-2")).not.toBeDefined()
})
})
describe("Events without subscribers", () => {
beforeEach(() => {
jest.clearAllMocks()
eventBus = new LocalEventBusService(moduleDeps as any, {}, {} as any)
eventEmitter = (eventBus as any).eventEmitter_
})
it("should not emit events when there are no subscribers", async () => {
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn(() => 0)
await eventBus.emit({
name: "eventWithoutSubscribers",
data: { test: "data" },
})
expect(eventEmitter.emit).not.toHaveBeenCalled()
})
it("should still call interceptors even when there are no subscribers", async () => {
const callInterceptorsSpy = jest.spyOn(
eventBus as any,
"callInterceptors"
)
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn(() => 0)
await eventBus.emit({
name: "eventWithoutSubscribers",
data: { test: "data" },
})
expect(callInterceptorsSpy).toHaveBeenCalledTimes(1)
expect(callInterceptorsSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: "eventWithoutSubscribers",
data: { test: "data" },
}),
{ isGrouped: false }
)
expect(eventEmitter.emit).not.toHaveBeenCalled()
callInterceptorsSpy.mockRestore()
})
it("should emit events with wildcard subscriber", async () => {
eventBus.subscribe("*", () => Promise.resolve())
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn((event) => (event === "*" ? 1 : 0))
await eventBus.emit({
name: "anyEvent",
data: { test: "data" },
})
// Wait for async emission to complete
await new Promise((resolve) => setImmediate(resolve))
expect(eventEmitter.emit).toHaveBeenCalledTimes(1)
expect(eventEmitter.emit).toHaveBeenCalledWith("*", {
data: { test: "data" },
name: "anyEvent",
})
})
it("should not emit grouped events when releasing if there are no subscribers", async () => {
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn(() => 0)
await eventBus.emit({
name: "grouped-event-no-sub",
data: { hi: "1234" },
metadata: { eventGroupId: "test-group-no-sub" },
})
expect(
(eventBus as any).groupedEventsMap_.get("test-group-no-sub")
).toHaveLength(1)
expect(eventEmitter.emit).not.toHaveBeenCalled()
jest.clearAllMocks()
eventEmitter.emit = jest.fn((data) => data)
await eventBus.releaseGroupedEvents("test-group-no-sub")
expect(eventEmitter.emit).not.toHaveBeenCalled()
expect(
(eventBus as any).groupedEventsMap_.get("test-group-no-sub")
).not.toBeDefined()
})
it("should still call interceptors for grouped events without subscribers", async () => {
eventEmitter.emit = jest.fn((data) => data)
eventEmitter.listenerCount = jest.fn(() => 0)
await eventBus.emit({
name: "grouped-event-no-sub-2",
data: { hi: "1234" },
metadata: { eventGroupId: "test-group-no-sub-2" },
})
expect(
(eventBus as any).groupedEventsMap_.get("test-group-no-sub-2")
).toHaveLength(1)
jest.clearAllMocks()
eventEmitter.emit = jest.fn((data) => data)
const callInterceptorsSpy = jest.spyOn(
eventBus as any,
"callInterceptors"
)
await eventBus.releaseGroupedEvents("test-group-no-sub-2")
expect(callInterceptorsSpy).toHaveBeenCalledTimes(1)
expect(callInterceptorsSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: "grouped-event-no-sub-2",
}),
{ isGrouped: true, eventGroupId: "test-group-no-sub-2" }
)
expect(eventEmitter.emit).not.toHaveBeenCalled()
callInterceptorsSpy.mockRestore()
})
})
})
})
@@ -58,11 +58,6 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
const eventListenersCount = this.eventEmitter_.listenerCount(
eventData.name
)
const startSubscribersCount = this.eventEmitter_.listenerCount("*")
if (eventListenersCount === 0 && startSubscribersCount === 0) {
continue
}
if (!options.internal && !eventData.options?.internal) {
this.logger_?.info(
@@ -93,11 +88,18 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
const options_ = eventData.options as { delay: number }
const delay = (ms?: number) => (ms ? setTimeout(ms) : Promise.resolve())
const eventListenersCount = this.eventEmitter_.listenerCount(
eventData.name
)
delay(options_?.delay).then(async () => {
// Call interceptors before emitting
void this.callInterceptors(eventData, { isGrouped: false })
this.eventEmitter_.emit(eventData.name, eventBody)
if (eventListenersCount) {
this.eventEmitter_.emit(eventData.name, eventBody)
}
if (hasStarSubscriber) {
this.eventEmitter_.emit("*", eventBody)
}
@@ -120,11 +122,14 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
async releaseGroupedEvents(eventGroupId: string) {
let groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []
groupedEvents = JSON.parse(JSON.stringify(groupedEvents))
const hasStarSubscriber = this.eventEmitter_.listenerCount("*") > 0
for (const event of groupedEvents) {
const { options, ...eventBody } = event
const eventListenersCount = this.eventEmitter_.listenerCount(event.name)
const options_ = options as { delay: number }
const delay = (ms?: number) => (ms ? setTimeout(ms) : Promise.resolve())
@@ -132,7 +137,10 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
// Call interceptors before emitting grouped events
void this.callInterceptors(event, { isGrouped: true, eventGroupId })
this.eventEmitter_.emit(event.name, eventBody)
if (eventListenersCount) {
this.eventEmitter_.emit(event.name, eventBody)
}
if (hasStarSubscriber) {
this.eventEmitter_.emit("*", eventBody)
}