feat(core, event-bus): Compensate emit event step utility (#13281)

* feat(core, event-bus): Compensate emit event step utility

* tests

* Update changeset to remove integration-tests-modules

Removed integration-tests-modules from changeset.

* revert test script
This commit is contained in:
Adrien de Peretti
2025-08-25 15:08:09 +02:00
committed by GitHub
parent 73a25abecb
commit fc49253273
11 changed files with 547 additions and 22 deletions
@@ -0,0 +1,141 @@
import {
CommonEvents,
composeMessage,
Modules,
} from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { IEventBusModuleService } from "@medusajs/types"
moduleIntegrationTestRunner<IEventBusModuleService>({
moduleName: Modules.EVENT_BUS,
testSuite: ({ service: eventBus }) => {
describe("Event Bus Local Service", () => {
it("should emit an event", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
expect(subscriber).toHaveBeenCalledWith({
data: {
test: "test",
},
metadata: {
source: "test",
object: "test",
action: "created",
},
name: "test",
})
eventBus.unsubscribe("test", subscriber)
})
it("should release grouped events", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
context: {
eventGroupId: "123",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
expect(subscriber).toHaveBeenCalledTimes(0)
await eventBus.releaseGroupedEvents("123")
expect(subscriber).toHaveBeenCalledTimes(1)
expect(subscriber).toHaveBeenCalledWith({
data: {
test: "test",
},
metadata: {
source: "test",
eventGroupId: "123",
object: "test",
action: "created",
},
name: "test",
})
eventBus.unsubscribe("test", subscriber)
})
it("should clear grouped events", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
context: {
eventGroupId: "123",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
expect(subscriber).toHaveBeenCalledTimes(0)
await eventBus.clearGroupedEvents("123")
await eventBus.releaseGroupedEvents("123")
expect(subscriber).toHaveBeenCalledTimes(0)
eventBus.unsubscribe("test", subscriber)
})
it("should clear grouped events with event names", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
context: {
eventGroupId: "123",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
await eventBus.clearGroupedEvents("123", {
eventNames: ["test"],
})
await eventBus.releaseGroupedEvents("123")
expect(subscriber).toHaveBeenCalledTimes(0)
eventBus.unsubscribe("test", subscriber)
})
})
},
})
@@ -33,7 +33,8 @@
"scripts": {
"watch": "tsc --build --watch",
"build": "rimraf dist && tsc --build",
"test": "jest --passWithNoTests"
"test": "jest --passWithNoTests",
"test:integration": "jest --no-cache --maxWorkers=50% --bail --detectOpenHandles --forceExit --logHeapUsage -- integration-tests/__tests__/**/*.spec.ts"
},
"dependencies": {
"ulid": "^2.3.0"
@@ -130,8 +130,19 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
await this.clearGroupedEvents(eventGroupId)
}
async clearGroupedEvents(eventGroupId: string) {
this.groupedEventsMap_.delete(eventGroupId)
async clearGroupedEvents(
eventGroupId: string,
{ eventNames }: { eventNames?: string[] } = {}
) {
if (eventNames?.length) {
const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []
const eventsToKeep = groupedEvents.filter(
(event) => !eventNames!.includes(event.name)
)
this.groupedEventsMap_.set(eventGroupId, eventsToKeep)
} else {
this.groupedEventsMap_.delete(eventGroupId)
}
}
subscribe(
@@ -0,0 +1,147 @@
import {
CommonEvents,
composeMessage,
Modules,
} from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { IEventBusModuleService } from "@medusajs/types"
moduleIntegrationTestRunner<IEventBusModuleService>({
moduleName: Modules.EVENT_BUS,
moduleOptions: {
redis: {
host: "localhost",
port: 6379,
},
},
testSuite: ({ service: eventBus }) => {
describe("Event Bus Redis Service", () => {
it("should emit an event", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
expect(subscriber).toHaveBeenCalledWith({
data: {
test: "test",
},
metadata: {
source: "test",
object: "test",
action: "created",
},
name: "test",
})
eventBus.unsubscribe("test", subscriber)
})
it("should release grouped events", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
context: {
eventGroupId: "123",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
expect(subscriber).toHaveBeenCalledTimes(0)
await eventBus.releaseGroupedEvents("123")
expect(subscriber).toHaveBeenCalledTimes(1)
expect(subscriber).toHaveBeenCalledWith({
data: {
test: "test",
},
metadata: {
source: "test",
eventGroupId: "123",
object: "test",
action: "created",
},
name: "test",
})
eventBus.unsubscribe("test", subscriber)
})
it("should clear grouped events", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
context: {
eventGroupId: "123",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
expect(subscriber).toHaveBeenCalledTimes(0)
await eventBus.clearGroupedEvents("123")
await eventBus.releaseGroupedEvents("123")
expect(subscriber).toHaveBeenCalledTimes(0)
eventBus.unsubscribe("test", subscriber)
})
it("should clear grouped events with event names", async () => {
const subscriber = jest.fn()
eventBus.subscribe("test", subscriber)
await eventBus.emit(
composeMessage("test", {
data: {
test: "test",
},
context: {
eventGroupId: "123",
},
action: CommonEvents.CREATED,
source: "test",
object: "test",
})
)
await eventBus.clearGroupedEvents("123", {
eventNames: ["test"],
})
await eventBus.releaseGroupedEvents("123")
expect(subscriber).toHaveBeenCalledTimes(0)
eventBus.unsubscribe("test", subscriber)
})
})
},
})
@@ -33,7 +33,8 @@
"scripts": {
"watch": "tsc --build --watch",
"build": "rimraf dist && tsc --build",
"test": "jest --silent --bail --maxWorkers=50% --forceExit"
"test": "jest --silent --bail --maxWorkers=50% --forceExit",
"test:integration": "jest --no-cache --maxWorkers=50% --bail --detectOpenHandles --forceExit --logHeapUsage -- integration-tests/__tests__/**/*.spec.ts"
},
"dependencies": {
"bullmq": "5.13.0",
@@ -218,11 +218,49 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
await this.clearGroupedEvents(eventGroupId)
}
async clearGroupedEvents(eventGroupId: string) {
async clearGroupedEvents(
eventGroupId: string,
{
eventNames,
}: {
eventNames?: string[]
} = {}
) {
if (!eventGroupId) {
return
}
if (eventNames?.length) {
/**
* If any event names are provided, we keep all events except the ones that match the event
* names. which allow to partially clear an event group.
*/
const eventsToKeep = await this.eventBusRedisConnection_
.lrange(`staging:${eventGroupId}`, 0, -1)
.then((result) => {
return result
.map((jsonString) => JSON.parse(jsonString))
.filter((event) => !eventNames.includes(event.name))
})
// Create a pipeline
const pipeline = this.eventBusRedisConnection_.pipeline()
// Empty the current list
pipeline.del(`staging:${eventGroupId}`)
// Add the remaining events to the list
pipeline.rpush(
`staging:${eventGroupId}`,
...eventsToKeep.map((event) => JSON.stringify(event))
)
await pipeline.exec()
return
}
await this.eventBusRedisConnection_.unlink(`staging:${eventGroupId}`)
}