chore: ability to group events on redis event bus (#7655)

* chore: ability to group events on redis event bus

* chore: fix tests

* Update packages/modules/event-bus-redis/src/services/event-bus-redis.ts

Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com>

* chore: change shape of input and body data

* chore: fix builds

* chore: address comments

* chore: fix unit test

---------

Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com>
This commit is contained in:
Riqwan Thamir
2024-06-10 22:15:43 +02:00
committed by GitHub
co-authored by Adrien de Peretti
parent 3b8160b564
commit 39ddba2491
24 changed files with 924 additions and 732 deletions
@@ -14,7 +14,8 @@ const moduleDeps = {
}
describe("LocalEventBusService", () => {
let eventBus
let eventBus: LocalEventBusService
let eventEmitter
describe("emit", () => {
describe("Successfully emits events", () => {
@@ -22,148 +23,184 @@ describe("LocalEventBusService", () => {
jest.clearAllMocks()
eventBus = new LocalEventBusService(moduleDeps as any)
eventEmitter = (eventBus as any).eventEmitter_
})
it("should emit an event", async () => {
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
eventEmitter.emit = jest.fn((data) => data)
await eventBus.emit("eventName", { hi: "1234" })
await eventBus.emit({
eventName: "eventName",
data: { hi: "1234" },
})
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1)
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("eventName", {
hi: "1234",
expect(eventEmitter.emit).toHaveBeenCalledTimes(1)
expect(eventEmitter.emit).toHaveBeenCalledWith("eventName", {
data: { hi: "1234" },
})
})
it("should emit multiple events", async () => {
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
eventEmitter.emit = jest.fn((data) => data)
await eventBus.emit([
{ eventName: "event-1", data: { hi: "1234" } },
{ eventName: "event-2", data: { hi: "5678" } },
])
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(2)
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-1", {
hi: "1234",
expect(eventEmitter.emit).toHaveBeenCalledTimes(2)
expect(eventEmitter.emit).toHaveBeenCalledWith("event-1", {
data: { hi: "1234" },
})
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-2", {
hi: "5678",
expect(eventEmitter.emit).toHaveBeenCalledWith("event-2", {
data: { hi: "5678" },
})
})
it("should group an event if data consists of eventGroupId", async () => {
const groupEventFn = jest.spyOn(eventBus, "groupEvent")
let groupEventFn = jest.spyOn(eventBus, "groupEvent" as any)
eventEmitter.emit = jest.fn((data) => data)
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
await eventBus.emit("test-event", {
test: "1234",
eventGroupId: "test",
await eventBus.emit({
eventName: "test-event",
data: {
test: "1234",
},
metadata: {
eventGroupId: "test",
},
})
expect(eventBus.eventEmitter_.emit).not.toHaveBeenCalled()
expect(eventEmitter.emit).not.toHaveBeenCalled()
expect(groupEventFn).toHaveBeenCalledTimes(1)
expect(groupEventFn).toHaveBeenCalledWith("test", "test-event", {
test: "1234",
expect(groupEventFn).toHaveBeenCalledWith("test", {
data: { test: "1234" },
metadata: { eventGroupId: "test" },
eventName: "test-event",
})
jest.clearAllMocks()
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
eventBus.emit("test-event", { test: "1234", eventGroupId: "test" })
eventBus.emit("test-event", { test: "test-1" })
groupEventFn = jest.spyOn(eventBus, "groupEvent" as any)
eventEmitter.emit = jest.fn((data) => data)
eventBus.emit([
{
eventName: "test-event",
data: { test: "1234" },
metadata: { eventGroupId: "test" },
},
{
eventName: "test-event",
data: { test: "test-1" },
},
])
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1)
expect(groupEventFn).toHaveBeenCalledTimes(1)
expect(eventBus.groupedEventsMap_.get("test")).toEqual([
expect((eventBus as any).groupedEventsMap_.get("test")).toEqual([
expect.objectContaining({ eventName: "test-event" }),
expect.objectContaining({ eventName: "test-event" }),
])
await eventBus.emit("test-event", {
test: "1234",
eventGroupId: "test-2",
await eventBus.emit({
eventName: "test-event",
data: { test: "1234" },
metadata: { eventGroupId: "test-2" },
})
expect(eventBus.groupedEventsMap_.get("test-2")).toEqual([
expect((eventBus as any).groupedEventsMap_.get("test-2")).toEqual([
expect.objectContaining({ eventName: "test-event" }),
])
})
it("should release events when requested with eventGroupId", async () => {
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
eventEmitter.emit = jest.fn((data) => data)
await eventBus.emit([
{
eventName: "event-1",
data: { test: "1", eventGroupId: "group-1" },
data: { test: "1" },
metadata: { eventGroupId: "group-1" },
},
{
eventName: "event-2",
data: { test: "2", eventGroupId: "group-1" },
data: { test: "2" },
metadata: { eventGroupId: "group-1" },
},
{
eventName: "event-1",
data: { test: "1", eventGroupId: "group-2" },
data: { test: "1" },
metadata: { eventGroupId: "group-2" },
},
{
eventName: "event-2",
data: { test: "2", eventGroupId: "group-2" },
data: { test: "2" },
metadata: { eventGroupId: "group-2" },
},
{ eventName: "event-1", data: { test: "1" } },
])
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1)
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-1", {
test: "1",
expect(eventEmitter.emit).toHaveBeenCalledTimes(1)
expect(eventEmitter.emit).toHaveBeenCalledWith("event-1", {
data: { test: "1" },
})
expect(eventBus.groupedEventsMap_.get("group-1")).toHaveLength(2)
expect(eventBus.groupedEventsMap_.get("group-2")).toHaveLength(2)
expect((eventBus as any).groupedEventsMap_.get("group-1")).toHaveLength(
2
)
expect((eventBus as any).groupedEventsMap_.get("group-2")).toHaveLength(
2
)
jest.clearAllMocks()
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
eventEmitter.emit = jest.fn((data) => data)
eventBus.releaseGroupedEvents("group-1")
expect(eventBus.groupedEventsMap_.get("group-1")).not.toBeDefined()
expect(eventBus.groupedEventsMap_.get("group-2")).toHaveLength(2)
expect(
(eventBus as any).groupedEventsMap_.get("group-1")
).not.toBeDefined()
expect((eventBus as any).groupedEventsMap_.get("group-2")).toHaveLength(
2
)
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(2)
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-1", {
test: "1",
expect(eventEmitter.emit).toHaveBeenCalledTimes(2)
expect(eventEmitter.emit).toHaveBeenCalledWith("event-1", {
data: { test: "1" },
})
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-2", {
test: "2",
expect(eventEmitter.emit).toHaveBeenCalledWith("event-2", {
data: { test: "2" },
})
})
it("should clear events from grouped events when requested with eventGroupId", async () => {
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
eventEmitter.emit = jest.fn((data) => data)
const getMap = () => (eventBus as any).groupedEventsMap_
await eventBus.emit([
{
eventName: "event-1",
data: { test: "1", eventGroupId: "group-1" },
data: { test: "1" },
metadata: { eventGroupId: "group-1" },
},
{
eventName: "event-1",
data: { test: "1", eventGroupId: "group-2" },
data: { test: "1" },
metadata: { eventGroupId: "group-2" },
},
])
expect(eventBus.groupedEventsMap_.get("group-1")).toHaveLength(1)
expect(eventBus.groupedEventsMap_.get("group-2")).toHaveLength(1)
expect(getMap().get("group-1")).toHaveLength(1)
expect(getMap().get("group-2")).toHaveLength(1)
eventBus.clearGroupedEvents("group-1")
expect(eventBus.groupedEventsMap_.get("group-1")).not.toBeDefined()
expect(eventBus.groupedEventsMap_.get("group-2")).toHaveLength(1)
expect(getMap().get("group-1")).not.toBeDefined()
expect(getMap().get("group-2")).toHaveLength(1)
eventBus.clearGroupedEvents("group-2")
expect(eventBus.groupedEventsMap_.get("group-2")).not.toBeDefined()
expect(getMap().get("group-2")).not.toBeDefined()
})
})
})
@@ -1,9 +1,9 @@
import { MedusaContainer } from "@medusajs/modules-sdk"
import {
EmitData,
EventBusTypes,
Logger,
Message,
MessageBody,
Subscriber,
} from "@medusajs/types"
import { AbstractEventBusModuleService } from "@medusajs/utils"
@@ -35,47 +35,28 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
this.groupedEventsMap_ = new Map()
}
async emit<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>
/**
* Emit a number of events
* @param {EmitData} data - the data to send to the subscriber.
*/
async emit<T>(data: EmitData<T>[]): Promise<void>
async emit<T>(data: Message<T>[]): Promise<void>
async emit<T, TInput extends string | EmitData<T>[] | Message<T>[] = string>(
eventOrData: TInput,
data?: T,
async emit<T = unknown>(
eventsData: Message<T> | Message<T>[],
options: Record<string, unknown> = {}
): Promise<void> {
const isBulkEmit = Array.isArray(eventOrData)
const normalizedEventsData = Array.isArray(eventsData)
? eventsData
: [eventsData]
const events: EmitData[] | Message<T>[] = isBulkEmit
? eventOrData
: [{ eventName: eventOrData, data }]
for (const event of events) {
for (const eventData of normalizedEventsData) {
const eventListenersCount = this.eventEmitter_.listenerCount(
event.eventName
eventData.eventName
)
this.logger_?.info(
`Processing ${event.eventName} which has ${eventListenersCount} subscribers`
`Processing ${eventData.eventName} which has ${eventListenersCount} subscribers`
)
if (eventListenersCount === 0) {
continue
}
const data = (event as EmitData).data ?? (event as Message<T>).body
await this.groupOrEmitEvent(event.eventName, data)
await this.groupOrEmitEvent(eventData)
}
}
@@ -84,28 +65,27 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
// explicitly requested.
// This is useful in the event of a distributed transaction where you'd want to emit
// events only once the transaction ends.
private async groupOrEmitEvent(
eventName: string,
data: unknown & { eventGroupId?: string }
) {
const { eventGroupId, ...eventData } = data
private async groupOrEmitEvent<T = unknown>(eventData: Message<T>) {
const { options, ...eventBody } = eventData
const eventGroupId = eventBody.metadata?.eventGroupId
if (eventGroupId) {
await this.groupEvent(eventGroupId, eventName, eventData)
await this.groupEvent(eventGroupId, eventData)
} else {
this.eventEmitter_.emit(eventName, data)
this.eventEmitter_.emit(eventData.eventName, {
data: eventData.data,
})
}
}
// Groups an event to a queue to be emitted upon explicit release
private async groupEvent(
private async groupEvent<T = unknown>(
eventGroupId: string,
eventName: string,
data: unknown
eventData: MessageBody<T>
) {
const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []
groupedEvents.push({ eventName, data })
groupedEvents.push(eventData)
this.groupedEventsMap_.set(eventGroupId, groupedEvents)
}
@@ -116,7 +96,7 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
for (const event of groupedEvents) {
const { eventName, data } = event
this.eventEmitter_.emit(eventName, data)
this.eventEmitter_.emit(eventName, { data })
}
this.clearGroupedEvents(eventGroupId)