chore: group & release events for local eventbus (#7649)
* chore: stage & release events for local eventbus * chore: address review * chore: mock emitter correctly
This commit is contained in:
@@ -24,6 +24,16 @@ export abstract class AbstractEventBusModuleService
|
|||||||
abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
|
abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
|
||||||
abstract emit<T>(data: EventBusTypes.Message<T>[]): Promise<void>
|
abstract emit<T>(data: EventBusTypes.Message<T>[]): Promise<void>
|
||||||
|
|
||||||
|
/*
|
||||||
|
Grouped events are useful when you have distributed transactions
|
||||||
|
where you need to explicitly group, release and clear events upon
|
||||||
|
lifecycle events of a transaction.
|
||||||
|
*/
|
||||||
|
// Given a eventGroupId, all the grouped events will be released
|
||||||
|
abstract releaseGroupedEvents(eventGroupId: string): Promise<void>
|
||||||
|
// Given a eventGroupId, all the grouped events will be cleared
|
||||||
|
abstract clearGroupedEvents(eventGroupId: string): Promise<void>
|
||||||
|
|
||||||
protected storeSubscribers({
|
protected storeSubscribers({
|
||||||
event,
|
event,
|
||||||
subscriberId,
|
subscriberId,
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import LocalEventBusService from "../event-bus-local"
|
|
||||||
|
|
||||||
jest.genMockFromModule("events")
|
|
||||||
jest.mock("events")
|
|
||||||
|
|
||||||
const loggerMock = {
|
|
||||||
info: jest.fn().mockReturnValue(console.log),
|
|
||||||
warn: jest.fn().mockReturnValue(console.log),
|
|
||||||
error: jest.fn().mockReturnValue(console.log),
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleDeps = {
|
|
||||||
logger: loggerMock,
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("LocalEventBusService", () => {
|
|
||||||
let eventBus
|
|
||||||
|
|
||||||
describe("emit", () => {
|
|
||||||
describe("Successfully emits events", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Emits an event", () => {
|
|
||||||
eventBus = new LocalEventBusService(
|
|
||||||
moduleDeps,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
resources: "shared",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
|
||||||
|
|
||||||
eventBus.emit("eventName", { hi: "1234" })
|
|
||||||
|
|
||||||
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1)
|
|
||||||
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("eventName", {
|
|
||||||
hi: "1234",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Emits multiple events", () => {
|
|
||||||
eventBus = new LocalEventBusService(
|
|
||||||
moduleDeps,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
resources: "shared",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
|
||||||
|
|
||||||
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(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-2", {
|
|
||||||
hi: "5678",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import LocalEventBusService from "../event-bus-local"
|
||||||
|
|
||||||
|
jest.genMockFromModule("events")
|
||||||
|
jest.mock("events")
|
||||||
|
|
||||||
|
const loggerMock = {
|
||||||
|
info: jest.fn().mockReturnValue(console.log),
|
||||||
|
warn: jest.fn().mockReturnValue(console.log),
|
||||||
|
error: jest.fn().mockReturnValue(console.log),
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleDeps = {
|
||||||
|
logger: loggerMock,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("LocalEventBusService", () => {
|
||||||
|
let eventBus
|
||||||
|
|
||||||
|
describe("emit", () => {
|
||||||
|
describe("Successfully emits events", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
eventBus = new LocalEventBusService(moduleDeps as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should emit an event", async () => {
|
||||||
|
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
||||||
|
|
||||||
|
await eventBus.emit("eventName", { hi: "1234" })
|
||||||
|
|
||||||
|
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("eventName", {
|
||||||
|
hi: "1234",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should emit multiple events", async () => {
|
||||||
|
eventBus.eventEmitter_.emit.mockImplementationOnce((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(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-2", {
|
||||||
|
hi: "5678",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should group an event if data consists of eventGroupId", async () => {
|
||||||
|
const groupEventFn = jest.spyOn(eventBus, "groupEvent")
|
||||||
|
|
||||||
|
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
||||||
|
|
||||||
|
await eventBus.emit("test-event", {
|
||||||
|
test: "1234",
|
||||||
|
eventGroupId: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(eventBus.eventEmitter_.emit).not.toHaveBeenCalled()
|
||||||
|
expect(groupEventFn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(groupEventFn).toHaveBeenCalledWith("test", "test-event", {
|
||||||
|
test: "1234",
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
||||||
|
eventBus.emit("test-event", { test: "1234", eventGroupId: "test" })
|
||||||
|
eventBus.emit("test-event", { test: "test-1" })
|
||||||
|
|
||||||
|
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(groupEventFn).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
expect(eventBus.groupedEventsMap_.get("test")).toEqual([
|
||||||
|
expect.objectContaining({ eventName: "test-event" }),
|
||||||
|
expect.objectContaining({ eventName: "test-event" }),
|
||||||
|
])
|
||||||
|
|
||||||
|
await eventBus.emit("test-event", {
|
||||||
|
test: "1234",
|
||||||
|
eventGroupId: "test-2",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(eventBus.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)
|
||||||
|
|
||||||
|
await eventBus.emit([
|
||||||
|
{
|
||||||
|
eventName: "event-1",
|
||||||
|
data: { test: "1", eventGroupId: "group-1" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventName: "event-2",
|
||||||
|
data: { test: "2", eventGroupId: "group-1" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventName: "event-1",
|
||||||
|
data: { test: "1", eventGroupId: "group-2" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventName: "event-2",
|
||||||
|
data: { test: "2", 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(eventBus.groupedEventsMap_.get("group-1")).toHaveLength(2)
|
||||||
|
expect(eventBus.groupedEventsMap_.get("group-2")).toHaveLength(2)
|
||||||
|
|
||||||
|
jest.clearAllMocks()
|
||||||
|
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
||||||
|
eventBus.releaseGroupedEvents("group-1")
|
||||||
|
|
||||||
|
expect(eventBus.groupedEventsMap_.get("group-1")).not.toBeDefined()
|
||||||
|
expect(eventBus.groupedEventsMap_.get("group-2")).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledTimes(2)
|
||||||
|
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-1", {
|
||||||
|
test: "1",
|
||||||
|
})
|
||||||
|
expect(eventBus.eventEmitter_.emit).toHaveBeenCalledWith("event-2", {
|
||||||
|
test: "2",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should clear events from grouped events when requested with eventGroupId", async () => {
|
||||||
|
eventBus.eventEmitter_.emit.mockImplementationOnce((data) => data)
|
||||||
|
|
||||||
|
await eventBus.emit([
|
||||||
|
{
|
||||||
|
eventName: "event-1",
|
||||||
|
data: { test: "1", eventGroupId: "group-1" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventName: "event-1",
|
||||||
|
data: { test: "1", eventGroupId: "group-2" },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(eventBus.groupedEventsMap_.get("group-1")).toHaveLength(1)
|
||||||
|
expect(eventBus.groupedEventsMap_.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)
|
||||||
|
|
||||||
|
eventBus.clearGroupedEvents("group-2")
|
||||||
|
|
||||||
|
expect(eventBus.groupedEventsMap_.get("group-2")).not.toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,6 +14,8 @@ type InjectedDependencies = {
|
|||||||
logger: Logger
|
logger: Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StagingQueueType = Map<string, { eventName: string; data?: unknown }[]>
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter()
|
const eventEmitter = new EventEmitter()
|
||||||
eventEmitter.setMaxListeners(Infinity)
|
eventEmitter.setMaxListeners(Infinity)
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ eventEmitter.setMaxListeners(Infinity)
|
|||||||
export default class LocalEventBusService extends AbstractEventBusModuleService {
|
export default class LocalEventBusService extends AbstractEventBusModuleService {
|
||||||
protected readonly logger_?: Logger
|
protected readonly logger_?: Logger
|
||||||
protected readonly eventEmitter_: EventEmitter
|
protected readonly eventEmitter_: EventEmitter
|
||||||
|
protected groupedEventsMap_: StagingQueueType
|
||||||
|
|
||||||
constructor({ logger }: MedusaContainer & InjectedDependencies) {
|
constructor({ logger }: MedusaContainer & InjectedDependencies) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -29,6 +32,7 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
|||||||
|
|
||||||
this.logger_ = logger
|
this.logger_ = logger
|
||||||
this.eventEmitter_ = eventEmitter
|
this.eventEmitter_ = eventEmitter
|
||||||
|
this.groupedEventsMap_ = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
async emit<T>(
|
async emit<T>(
|
||||||
@@ -70,10 +74,58 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = (event as EmitData).data ?? (event as Message<T>).body
|
const data = (event as EmitData).data ?? (event as Message<T>).body
|
||||||
this.eventEmitter_.emit(event.eventName, data)
|
|
||||||
|
await this.groupOrEmitEvent(event.eventName, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the data of the event consists of a eventGroupId, we don't emit the event, instead
|
||||||
|
// we add them to a queue grouped by the eventGroupId and release them when
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if (eventGroupId) {
|
||||||
|
await this.groupEvent(eventGroupId, eventName, eventData)
|
||||||
|
} else {
|
||||||
|
this.eventEmitter_.emit(eventName, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups an event to a queue to be emitted upon explicit release
|
||||||
|
private async groupEvent(
|
||||||
|
eventGroupId: string,
|
||||||
|
eventName: string,
|
||||||
|
data: unknown
|
||||||
|
) {
|
||||||
|
const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []
|
||||||
|
|
||||||
|
groupedEvents.push({ eventName, data })
|
||||||
|
|
||||||
|
this.groupedEventsMap_.set(eventGroupId, groupedEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseGroupedEvents(eventGroupId: string) {
|
||||||
|
const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []
|
||||||
|
|
||||||
|
for (const event of groupedEvents) {
|
||||||
|
const { eventName, data } = event
|
||||||
|
|
||||||
|
this.eventEmitter_.emit(eventName, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearGroupedEvents(eventGroupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearGroupedEvents(eventGroupId: string) {
|
||||||
|
this.groupedEventsMap_.delete(eventGroupId)
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||||
const randId = ulid()
|
const randId = ulid()
|
||||||
this.storeSubscribers({ event, subscriberId: randId, subscriber })
|
this.storeSubscribers({ event, subscriberId: randId, subscriber })
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
|
|||||||
await this.queue_.addBulk(events)
|
await this.queue_.addBulk(events)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Implement redis based staging + release
|
||||||
|
async releaseGroupedEvents(eventGroupId: string) {}
|
||||||
|
async clearGroupedEvents(eventGroupId: string) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles incoming jobs.
|
* Handles incoming jobs.
|
||||||
* @param job The job object
|
* @param job The job object
|
||||||
|
|||||||
Reference in New Issue
Block a user