refactor: split monolith into 3 microservices (frontend, backend, discord-gateway)
- Extract services into services/{frontend,backend,discord-gateway}
- Create packages/shared/ for shared logger, errors, utils, types
- Setup Modular MVC pattern in backend (controller→service→repository)
- Setup event-driven architecture in discord-gateway with Redis pub/sub
- Move Docker files to infra/docker/ with per-service Dockerfiles
- Update docker-compose.yml to use Traefik-only routing (no port exposes)
- Update GitHub Actions deploy workflow for multi-service matrix build
- Fix all import paths and resolve type errors across all services
- All 3 services pass tsc --noEmit clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bda8304bb9
commit
c48a0c5e3b
@@ -0,0 +1,146 @@
|
||||
import Redis from "ioredis";
|
||||
import type { CustomLogger } from "../../shared/logger/logger.js";
|
||||
|
||||
export interface DiscordGatewayEvent {
|
||||
type: string;
|
||||
data: unknown;
|
||||
timestamp: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export class RedisEventPublisher {
|
||||
private redis: Redis;
|
||||
private logger: CustomLogger;
|
||||
|
||||
constructor(redisUrl: string, logger: CustomLogger) {
|
||||
this.redis = new Redis(redisUrl);
|
||||
this.logger = logger;
|
||||
|
||||
this.redis.on("error", (err) => {
|
||||
this.logger.error({ error: err }, "Redis connection error");
|
||||
});
|
||||
|
||||
this.redis.on("connect", () => {
|
||||
this.logger.info("Redis connected");
|
||||
});
|
||||
}
|
||||
|
||||
async publish(channel: string, event: DiscordGatewayEvent): Promise<void> {
|
||||
try {
|
||||
await this.redis.publish(channel, JSON.stringify(event));
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
{ error, channel, eventType: event.type },
|
||||
"Failed to publish event",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.redis.quit();
|
||||
}
|
||||
}
|
||||
|
||||
export class EventBroadcaster {
|
||||
private publisher: RedisEventPublisher;
|
||||
private logger: CustomLogger;
|
||||
|
||||
constructor(publisher: RedisEventPublisher, logger: CustomLogger) {
|
||||
this.publisher = publisher;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async messageCreated(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:created", {
|
||||
type: "message_created",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async messageUpdated(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:updated", {
|
||||
type: "message_updated",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async messageDeleted(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:deleted", {
|
||||
type: "message_deleted",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async messageAnalyzed(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:message:analyzed", {
|
||||
type: "message_analyzed",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async attachmentCreated(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:attachment:created", {
|
||||
type: "attachment_created",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async attachmentUploaded(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:attachment:uploaded", {
|
||||
type: "attachment_uploaded",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async voiceRecordingStarted(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:started", {
|
||||
type: "voice_recording_started",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async voiceRecordingStopped(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:stopped", {
|
||||
type: "voice_recording_stopped",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async voiceRecordingUploaded(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:voice:uploaded", {
|
||||
type: "voice_recording_uploaded",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async analysisQueueStatus(data: unknown): Promise<void> {
|
||||
await this.publisher.publish("discord:analysis:queue_status", {
|
||||
type: "analysis_queue_status",
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: "discord-gateway",
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.publisher.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface DiscordGatewayEvent {
|
||||
type: string;
|
||||
data: unknown;
|
||||
timestamp: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export const EventChannels = {
|
||||
MESSAGE_CREATED: "discord:message:created",
|
||||
MESSAGE_UPDATED: "discord:message:updated",
|
||||
MESSAGE_DELETED: "discord:message:deleted",
|
||||
MESSAGE_ANALYZED: "discord:message:analyzed",
|
||||
ATTACHMENT_CREATED: "discord:attachment:created",
|
||||
ATTACHMENT_UPLOADED: "discord:attachment:uploaded",
|
||||
VOICE_STARTED: "discord:voice:started",
|
||||
VOICE_STOPPED: "discord:voice:stopped",
|
||||
VOICE_UPLOADED: "discord:voice:uploaded",
|
||||
ANALYSIS_QUEUE_STATUS: "discord:analysis:queue_status",
|
||||
} as const;
|
||||
|
||||
export type EventChannelType =
|
||||
(typeof EventChannels)[keyof typeof EventChannels];
|
||||
@@ -0,0 +1,6 @@
|
||||
export { EventBroadcaster, RedisEventPublisher } from "./eventBroadcaster.js";
|
||||
export {
|
||||
type DiscordGatewayEvent,
|
||||
EventChannels,
|
||||
type EventChannelType,
|
||||
} from "./eventTypes.js";
|
||||
Reference in New Issue
Block a user