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:
co-authored by
Adrien de Peretti
parent
3b8160b564
commit
39ddba2491
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user