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
@@ -3,13 +3,6 @@ import { Queue, Worker } from "bullmq"
import { Redis } from "ioredis"
import RedisEventBusService from "../event-bus-redis"
// const redisURL = "redis://localhost:6379"
// const client = new Redis(6379, redisURL, {
// // Lazy connect to properly handle connection errors
// lazyConnect: true,
// maxRetriesPerRequest: 0,
// })
jest.mock("bullmq")
jest.mock("ioredis")
@@ -96,6 +89,8 @@ describe("RedisEventBusService", () => {
})
it("should add job to queue with default options", async () => {
eventBus.subscribe("eventName", () => Promise.resolve())
await eventBus.emit([
{
name: "eventName",
@@ -119,6 +114,8 @@ describe("RedisEventBusService", () => {
})
it("should add job to queue with custom options passed directly upon emitting", async () => {
eventBus.subscribe("eventName", () => Promise.resolve())
await eventBus.emit([{ name: "eventName", data: { hi: "1234" } }], {
attempts: 3,
backoff: 5000,
@@ -158,6 +155,8 @@ describe("RedisEventBusService", () => {
queue = (eventBus as any).queue_
queue.addBulk = jest.fn()
eventBus.subscribe("eventName", () => Promise.resolve())
await eventBus.emit(
[
{
@@ -202,6 +201,8 @@ describe("RedisEventBusService", () => {
queue = (eventBus as any).queue_
queue.addBulk = jest.fn()
eventBus.subscribe("eventName", () => Promise.resolve())
await eventBus.emit(
{
name: "eventName",
@@ -268,12 +269,15 @@ describe("RedisEventBusService", () => {
},
]
eventBus.subscribe("ungrouped-event-2", () => Promise.resolve())
eventBus.subscribe("grouped-event-1", () => Promise.resolve())
eventBus.subscribe("grouped-event-2", () => Promise.resolve())
eventBus.subscribe("grouped-event-3", () => Promise.resolve())
redis.unlink = jest.fn()
await eventBus.emit(events, options)
// Expect 1 event to have been send
// Expect 2 pushes to redis as there are 2 groups of events to push
expect(queue.addBulk).toHaveBeenCalledTimes(1)
expect(redis.rpush).toHaveBeenCalledTimes(2)
expect(redis.unlink).not.toHaveBeenCalled()
@@ -331,6 +335,149 @@ describe("RedisEventBusService", () => {
expect(redis.unlink).toHaveBeenCalledWith("staging:test-group-2")
})
})
describe("Events without subscribers", () => {
beforeEach(async () => {
jest.clearAllMocks()
eventBus = new RedisEventBusService(moduleDeps, simpleModuleOptions, {
scope: "internal",
})
queue = (eventBus as any).queue_
queue.addBulk = jest.fn()
redis = (eventBus as any).eventBusRedisConnection_
redis.rpush = jest.fn()
})
it("should not add events to queue when there are no subscribers", async () => {
await eventBus.emit([
{
name: "eventWithoutSubscribers",
data: { test: "data" },
},
])
expect(queue.addBulk).not.toHaveBeenCalled()
})
it("should still call interceptors even when there are no subscribers", async () => {
const callInterceptorsSpy = jest.spyOn(
eventBus as any,
"callInterceptors"
)
await eventBus.emit([
{
name: "eventWithoutSubscribers",
data: { test: "data" },
},
])
expect(callInterceptorsSpy).toHaveBeenCalledTimes(1)
expect(callInterceptorsSpy).toHaveBeenCalledWith(
{
name: "eventWithoutSubscribers",
data: { test: "data" },
},
{ isGrouped: false }
)
expect(queue.addBulk).not.toHaveBeenCalled()
callInterceptorsSpy.mockRestore()
})
it("should add events to queue only for events with subscribers using wildcard", async () => {
eventBus.subscribe("*", () => Promise.resolve())
await eventBus.emit([
{
name: "anyEvent",
data: { test: "data" },
},
])
expect(queue.addBulk).toHaveBeenCalledTimes(1)
})
it("should not add grouped events to queue when releasing if there are no subscribers", async () => {
const options = { delay: 1000 }
const event = {
name: "grouped-event-no-sub",
data: { hi: "1234" },
metadata: { eventGroupId: "test-group-no-sub" },
}
await eventBus.emit(event, options)
expect(redis.rpush).toHaveBeenCalledTimes(1)
expect(queue.addBulk).not.toHaveBeenCalled()
const [builtEvent] = (eventBus as any).buildEvents([event], options)
redis.lrange = jest.fn((key) => {
if (key === "staging:test-group-no-sub") {
return Promise.resolve([JSON.stringify(builtEvent)])
}
return Promise.resolve([])
})
redis.unlink = jest.fn()
queue.addBulk = jest.fn()
await eventBus.releaseGroupedEvents("test-group-no-sub")
expect(queue.addBulk).not.toHaveBeenCalled()
expect(redis.unlink).toHaveBeenCalledWith("staging:test-group-no-sub")
})
it("should still call interceptors for grouped events without subscribers", async () => {
const options = { delay: 1000 }
const event = {
name: "grouped-event-no-sub-2",
data: { hi: "1234" },
metadata: { eventGroupId: "test-group-no-sub-2" },
}
await eventBus.emit(event, options)
const [builtEvent] = (eventBus as any).buildEvents([event], options)
redis.lrange = jest.fn((key) => {
if (key === "staging:test-group-no-sub-2") {
return Promise.resolve([JSON.stringify(builtEvent)])
}
return Promise.resolve([])
})
redis.unlink = jest.fn()
queue.addBulk = jest.fn()
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(queue.addBulk).not.toHaveBeenCalled()
callInterceptorsSpy.mockRestore()
})
})
})
describe("worker_", () => {
@@ -352,7 +499,6 @@ describe("RedisEventBusService", () => {
return Promise.resolve()
})
// TODO: The typing for this is all over the place
await eventBus.worker_({
name: "eventName",
data: { data: { test: 1 } },
@@ -162,9 +162,17 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
this.callInterceptors(eventData, { isGrouped: false })
)
const emitData = this.buildEvents(eventsToEmit, options)
const eventsWithSubscribers = eventsToEmit.filter((eventData) => {
const eventSubscribers =
this.eventToSubscribersMap.get(eventData.name) || []
const wildcardSubscribers = this.eventToSubscribersMap.get("*") || []
return eventSubscribers.length || wildcardSubscribers.length
})
promises.push(this.queue_.addBulk(emitData))
if (eventsWithSubscribers.length) {
const emitData = this.buildEvents(eventsWithSubscribers, options)
promises.push(this.queue_.addBulk(emitData))
}
}
for (const [groupId, events] of groupEventsMap.entries()) {
@@ -220,7 +228,6 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
// Call interceptors before emitting grouped events
// Extract the original messages from the job data structure
groupedEvents.map((jobData) => {
// Reconstruct the message from the job data
const message = {
name: jobData.name,
data: jobData.data,
@@ -232,7 +239,16 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
})
})
await this.queue_.addBulk(groupedEvents)
const eventsWithSubscribers = groupedEvents.filter((jobData) => {
const eventSubscribers =
this.eventToSubscribersMap.get(jobData.name) || []
const wildcardSubscribers = this.eventToSubscribersMap.get("*") || []
return eventSubscribers.length || wildcardSubscribers.length
})
if (eventsWithSubscribers.length) {
await this.queue_.addBulk(eventsWithSubscribers)
}
await this.clearGroupedEvents(eventGroupId)
}