refactor: replace dynamic imports with event bus pattern
Replace 12 dynamic `await import()` calls with domain events emitted through the existing systemEvents bus, breaking circular dependencies between services (economy/inventory/leveling -> quest, * -> dashboard). - Add `emitAsync` to SystemEventEmitter for sequential listener awaiting, preserving DB transaction atomicity for quest progress tracking - Add DOMAIN event constants (BALANCE_CHANGED, XP_GAINED, ITEM_COLLECTED, ITEM_USED, TRANSFER_COMPLETED, DAILY_CLAIMED, TRIVIA_*, EXAM_PASSED) - Create shared/lib/eventWiring.ts to register all domain event listeners - Convert quest event calls to `await systemEvents.emitAsync()` (5 calls) - Convert dashboard event calls to `systemEvents.emit()` fire-and-forget (5 calls) - Convert exam.service.ts userService import to static import (1 call) - Convert dashboard.service.ts events import to static import (1 call) - Leave inventory.service.ts validateAndExecuteEffect import unchanged (Task 3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,16 @@ import { AuroraClient } from "@/lib/BotClient";
|
|||||||
import { env } from "@shared/lib/env";
|
import { env } from "@shared/lib/env";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { initializeConfig } from "@shared/lib/config";
|
import { initializeConfig } from "@shared/lib/config";
|
||||||
|
import { registerDomainEventListeners } from "@shared/lib/eventWiring";
|
||||||
|
|
||||||
import { startWebServerFromRoot } from "../api/src/server";
|
import { startWebServerFromRoot } from "../api/src/server";
|
||||||
|
|
||||||
// Initialize config from database
|
// Initialize config from database
|
||||||
await initializeConfig();
|
await initializeConfig();
|
||||||
|
|
||||||
|
// Register domain event listeners before loading commands/events
|
||||||
|
registerDomainEventListeners();
|
||||||
|
|
||||||
// Load commands & events
|
// Load commands & events
|
||||||
await AuroraClient.loadCommands();
|
await AuroraClient.loadCommands();
|
||||||
await AuroraClient.loadEvents();
|
await AuroraClient.loadEvents();
|
||||||
|
|||||||
72
shared/lib/eventWiring.ts
Normal file
72
shared/lib/eventWiring.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
|
import { questService } from "@shared/modules/quest/quest.service";
|
||||||
|
import { dashboardService } from "@shared/modules/dashboard/dashboard.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers all domain event listeners.
|
||||||
|
* Must be called once at startup before any domain events are emitted.
|
||||||
|
*
|
||||||
|
* This wiring replaces dynamic imports that were previously used to avoid
|
||||||
|
* circular dependencies between services (e.g., economy -> quest -> economy).
|
||||||
|
*/
|
||||||
|
export function registerDomainEventListeners() {
|
||||||
|
// --- Quest progress tracking (awaited via emitAsync to preserve tx atomicity) ---
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.BALANCE_CHANGED, async ({ userId, type, tx }) => {
|
||||||
|
await questService.handleEvent(userId, type, 1, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.XP_GAINED, async ({ userId, amount, tx }) => {
|
||||||
|
await questService.handleEvent(userId, 'XP_GAIN', amount, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.ITEM_COLLECTED, async ({ userId, itemId, quantity, tx }) => {
|
||||||
|
await questService.handleEvent(userId, `ITEM_COLLECT:${itemId}`, quantity, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.ITEM_USED, async ({ userId, itemId, tx }) => {
|
||||||
|
await questService.handleEvent(userId, `ITEM_USE:${itemId}`, 1, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Dashboard event recording (fire-and-forget) ---
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.TRANSFER_COMPLETED, async ({ username, amount, toUserId }) => {
|
||||||
|
await dashboardService.recordEvent({
|
||||||
|
type: 'info',
|
||||||
|
message: `${username} transferred ${amount.toLocaleString()} AU to User ID ${toUserId}`,
|
||||||
|
icon: '💸'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.DAILY_CLAIMED, async ({ username, amount }) => {
|
||||||
|
await dashboardService.recordEvent({
|
||||||
|
type: 'success',
|
||||||
|
message: `${username} claimed daily reward: ${amount.toLocaleString()} AU`,
|
||||||
|
icon: '☀️'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.TRIVIA_STARTED, async ({ username, difficulty }) => {
|
||||||
|
await dashboardService.recordEvent({
|
||||||
|
type: 'info',
|
||||||
|
message: `${username} started a trivia game (${difficulty})`,
|
||||||
|
icon: '🎯'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.TRIVIA_WON, async ({ username, reward }) => {
|
||||||
|
await dashboardService.recordEvent({
|
||||||
|
type: 'success',
|
||||||
|
message: `${username} won ${reward.toLocaleString()} AU from trivia!`,
|
||||||
|
icon: '🎉'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
systemEvents.on(EVENTS.DOMAIN.EXAM_PASSED, async ({ username, reward }) => {
|
||||||
|
await dashboardService.recordEvent({
|
||||||
|
type: 'success',
|
||||||
|
message: `${username} passed their exam: ${reward.toLocaleString()} AU`,
|
||||||
|
icon: '🎓'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,9 +2,22 @@ import { EventEmitter } from "node:events";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Global system event bus for cross-module communication.
|
* Global system event bus for cross-module communication.
|
||||||
* Used primarily for real-time dashboard updates.
|
* Used for real-time dashboard updates and domain event decoupling.
|
||||||
*/
|
*/
|
||||||
class SystemEventEmitter extends EventEmitter { }
|
class SystemEventEmitter extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Emit an event and await all listeners sequentially.
|
||||||
|
* Used for domain events that must preserve transaction atomicity
|
||||||
|
* (e.g., quest progress tracking within the caller's DB transaction).
|
||||||
|
*/
|
||||||
|
async emitAsync(event: string, ...args: any[]): Promise<boolean> {
|
||||||
|
const listeners = this.listeners(event);
|
||||||
|
for (const listener of listeners) {
|
||||||
|
await (listener as Function)(...args);
|
||||||
|
}
|
||||||
|
return listeners.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const systemEvents = new SystemEventEmitter();
|
export const systemEvents = new SystemEventEmitter();
|
||||||
|
|
||||||
@@ -20,5 +33,16 @@ export const EVENTS = {
|
|||||||
},
|
},
|
||||||
QUEST: {
|
QUEST: {
|
||||||
COMPLETED: "quest:completed",
|
COMPLETED: "quest:completed",
|
||||||
}
|
},
|
||||||
|
DOMAIN: {
|
||||||
|
BALANCE_CHANGED: "domain:balance_changed",
|
||||||
|
XP_GAINED: "domain:xp_gained",
|
||||||
|
ITEM_COLLECTED: "domain:item_collected",
|
||||||
|
ITEM_USED: "domain:item_used",
|
||||||
|
TRANSFER_COMPLETED: "domain:transfer_completed",
|
||||||
|
DAILY_CLAIMED: "domain:daily_claimed",
|
||||||
|
TRIVIA_STARTED: "domain:trivia_started",
|
||||||
|
TRIVIA_WON: "domain:trivia_won",
|
||||||
|
EXAM_PASSED: "domain:exam_passed",
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||||
import { users, transactions, moderationCases, inventory, lootdrops, items, type User } from "@db/schema";
|
import { users, transactions, moderationCases, inventory, lootdrops, items, type User } from "@db/schema";
|
||||||
import { desc, sql, gte, eq } from "drizzle-orm";
|
import { desc, sql, gte, eq } from "drizzle-orm";
|
||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
import type { RecentEvent, ActivityData } from "./dashboard.types";
|
import type { RecentEvent, ActivityData } from "./dashboard.types";
|
||||||
import { TransactionType } from "@shared/lib/constants";
|
import { TransactionType } from "@shared/lib/constants";
|
||||||
|
|
||||||
@@ -139,7 +140,6 @@ export const dashboardService = {
|
|||||||
|
|
||||||
// Broadcast to WebSocket clients
|
// Broadcast to WebSocket clients
|
||||||
try {
|
try {
|
||||||
const { systemEvents, EVENTS } = await import("@shared/lib/events");
|
|
||||||
systemEvents.emit(EVENTS.DASHBOARD.NEW_EVENT, {
|
systemEvents.emit(EVENTS.DASHBOARD.NEW_EVENT, {
|
||||||
...fullEvent,
|
...fullEvent,
|
||||||
timestamp: (fullEvent.timestamp instanceof Date)
|
timestamp: (fullEvent.timestamp instanceof Date)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { users, transactions, userTimers } from "@db/schema";
|
import { users, transactions, userTimers } from "@db/schema";
|
||||||
import { eq, sql, and } from "drizzle-orm";
|
import { eq, sql, and } from "drizzle-orm";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError } from "@shared/lib/errors";
|
||||||
@@ -62,12 +63,7 @@ export const economyService = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Record dashboard event
|
// Record dashboard event
|
||||||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
systemEvents.emit(EVENTS.DOMAIN.TRANSFER_COMPLETED, { username: sender.username, amount, toUserId });
|
||||||
await dashboardService.recordEvent({
|
|
||||||
type: 'info',
|
|
||||||
message: `${sender.username} transferred ${amount.toLocaleString()} AU to User ID ${toUserId}`,
|
|
||||||
icon: '💸'
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, amount };
|
return { success: true, amount };
|
||||||
}, tx);
|
}, tx);
|
||||||
@@ -158,12 +154,7 @@ export const economyService = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Record dashboard event
|
// Record dashboard event
|
||||||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
systemEvents.emit(EVENTS.DOMAIN.DAILY_CLAIMED, { username: user.username, amount: totalReward });
|
||||||
await dashboardService.recordEvent({
|
|
||||||
type: 'success',
|
|
||||||
message: `${user.username} claimed daily reward: ${totalReward.toLocaleString()} AU`,
|
|
||||||
icon: '☀️'
|
|
||||||
});
|
|
||||||
|
|
||||||
return { claimed: true, amount: totalReward, streak, nextReadyAt, isWeekly: isWeeklyCurrent, weeklyBonus: weeklyBonusAmount };
|
return { claimed: true, amount: totalReward, streak, nextReadyAt, isWeekly: isWeeklyCurrent, weeklyBonus: weeklyBonusAmount };
|
||||||
}, tx);
|
}, tx);
|
||||||
@@ -197,8 +188,7 @@ export const economyService = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Trigger Quest Event
|
// Trigger Quest Event
|
||||||
const { questService } = await import("@shared/modules/quest/quest.service");
|
await systemEvents.emitAsync(EVENTS.DOMAIN.BALANCE_CHANGED, { userId: id, type, tx: txFn });
|
||||||
await questService.handleEvent(id, type, 1, txFn);
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}, tx);
|
}, tx);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { users, userTimers, transactions } from "@db/schema";
|
|||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import { TimerType, TransactionType } from "@shared/lib/constants";
|
import { TimerType, TransactionType } from "@shared/lib/constants";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
|
import { userService } from "@shared/modules/user/user.service";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { UserError, SystemError } from "@shared/lib/errors";
|
import { UserError, SystemError } from "@shared/lib/errors";
|
||||||
@@ -84,7 +86,6 @@ export const examService = {
|
|||||||
async registerForExam(userId: string, username: string, tx?: Transaction): Promise<ExamActionResult> {
|
async registerForExam(userId: string, username: string, tx?: Transaction): Promise<ExamActionResult> {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// Ensure user exists
|
// Ensure user exists
|
||||||
const { userService } = await import("@shared/modules/user/user.service");
|
|
||||||
const user = await userService.getOrCreateUser(userId, username, txFn);
|
const user = await userService.getOrCreateUser(userId, username, txFn);
|
||||||
if (!user) throw new SystemError("Failed to get or create user.");
|
if (!user) throw new SystemError("Failed to get or create user.");
|
||||||
|
|
||||||
@@ -242,12 +243,7 @@ export const examService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record dashboard event
|
// Record dashboard event
|
||||||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
systemEvents.emit(EVENTS.DOMAIN.EXAM_PASSED, { username: user.username, reward });
|
||||||
await dashboardService.recordEvent({
|
|
||||||
type: 'success',
|
|
||||||
message: `${user.username} passed their exam: ${reward.toLocaleString()} AU`,
|
|
||||||
icon: '🎓'
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: ExamStatus.AVAILABLE,
|
status: ExamStatus.AVAILABLE,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DrizzleClient } from "@shared/db/DrizzleClient";
|
|||||||
import { economyService } from "@shared/modules/economy/economy.service";
|
import { economyService } from "@shared/modules/economy/economy.service";
|
||||||
import { levelingService } from "@shared/modules/leveling/leveling.service";
|
import { levelingService } from "@shared/modules/leveling/leveling.service";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError } from "@shared/lib/errors";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction, ItemUsageData } from "@shared/lib/types";
|
import type { Transaction, ItemUsageData } from "@shared/lib/types";
|
||||||
@@ -39,8 +40,7 @@ export const inventoryService = {
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Trigger Quest Event
|
// Trigger Quest Event
|
||||||
const { questService } = await import("@shared/modules/quest/quest.service");
|
await systemEvents.emitAsync(EVENTS.DOMAIN.ITEM_COLLECTED, { userId, itemId, quantity: Number(quantity), tx: txFn });
|
||||||
await questService.handleEvent(userId, `ITEM_COLLECT:${itemId}`, Number(quantity), txFn);
|
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
} else {
|
} else {
|
||||||
@@ -67,8 +67,7 @@ export const inventoryService = {
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Trigger Quest Event
|
// Trigger Quest Event
|
||||||
const { questService } = await import("@shared/modules/quest/quest.service");
|
await systemEvents.emitAsync(EVENTS.DOMAIN.ITEM_COLLECTED, { userId, itemId, quantity: Number(quantity), tx: txFn });
|
||||||
await questService.handleEvent(userId, `ITEM_COLLECT:${itemId}`, Number(quantity), txFn);
|
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@@ -184,8 +183,7 @@ export const inventoryService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger Quest Event
|
// Trigger Quest Event
|
||||||
const { questService } = await import("@shared/modules/quest/quest.service");
|
await systemEvents.emitAsync(EVENTS.DOMAIN.ITEM_USED, { userId, itemId, tx: txFn });
|
||||||
await questService.handleEvent(userId, `ITEM_USE:${itemId}`, 1, txFn);
|
|
||||||
|
|
||||||
return { success: true, results, usageData, item };
|
return { success: true, results, usageData, item };
|
||||||
}, tx);
|
}, tx);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { users, userTimers } from "@db/schema";
|
|||||||
import { eq, sql, and } from "drizzle-orm";
|
import { eq, sql, and } from "drizzle-orm";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { TimerKey, TimerType } from "@shared/lib/constants";
|
import { TimerKey, TimerType } from "@shared/lib/constants";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError } from "@shared/lib/errors";
|
||||||
@@ -70,8 +71,7 @@ export const levelingService = {
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Trigger Quest Event
|
// Trigger Quest Event
|
||||||
const { questService } = await import("@shared/modules/quest/quest.service");
|
await systemEvents.emitAsync(EVENTS.DOMAIN.XP_GAINED, { userId: id, amount: Number(amount), tx: txFn });
|
||||||
await questService.handleEvent(id, 'XP_GAIN', Number(amount), txFn);
|
|
||||||
|
|
||||||
return { user: updatedUser, levelUp, currentLevel: newLevel };
|
return { user: updatedUser, levelUp, currentLevel: newLevel };
|
||||||
}, tx);
|
}, tx);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { users, userTimers, transactions } from "@db/schema";
|
import { users, userTimers, transactions } from "@db/schema";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
import { withTransaction } from "@/lib/db";
|
import { withTransaction } from "@/lib/db";
|
||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
import { UserError, SystemError } from "@shared/lib/errors";
|
import { UserError, SystemError } from "@shared/lib/errors";
|
||||||
@@ -232,12 +233,7 @@ class TriviaService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Record dashboard event
|
// Record dashboard event
|
||||||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
systemEvents.emit(EVENTS.DOMAIN.TRIVIA_STARTED, { username, difficulty: question.difficulty });
|
||||||
await dashboardService.recordEvent({
|
|
||||||
type: 'info',
|
|
||||||
message: `${username} started a trivia game (${question.difficulty})`,
|
|
||||||
icon: '🎯'
|
|
||||||
});
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
});
|
});
|
||||||
@@ -293,12 +289,7 @@ class TriviaService {
|
|||||||
where: eq(users.id, BigInt(userId))
|
where: eq(users.id, BigInt(userId))
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
|
systemEvents.emit(EVENTS.DOMAIN.TRIVIA_WON, { username: user?.username, reward });
|
||||||
await dashboardService.recordEvent({
|
|
||||||
type: 'success',
|
|
||||||
message: `${user?.username} won ${reward.toLocaleString()} AU from trivia!`,
|
|
||||||
icon: '🎉'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user