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:
syntaxbullet
2026-03-18 12:59:15 +01:00
parent a96c6caa49
commit 5863418ae9
9 changed files with 120 additions and 45 deletions

72
shared/lib/eventWiring.ts Normal file
View 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: '🎓'
});
});
}

View File

@@ -2,9 +2,22 @@ import { EventEmitter } from "node:events";
/**
* 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();
@@ -20,5 +33,16 @@ export const EVENTS = {
},
QUEST: {
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;