Files
GMW/design/services/13-gateway-event-design.md
T
asepharyana d59b59a7a7 feat: migrate frontend to Astro + expand AI moderation + backend admin/runtime config
Frontend:
- migrate from Vite to Astro (astro.config.mjs, pages/, layouts/)
- add admin panel, settings page, command palette, error boundary
- refactor App.tsx, MascotChatbot, Sidebar, Header, DashboardLayout
- update API client, WebSocket, auth, dashboard features

Backend:
- add admin module and config routes
- refactor middlewares, Redis connection, WebSocket server/bridge
- add runtime config loader

Discord Gateway:
- refactor AI moderation: circuit breaker, concurrency limiter, fallback processor
- add media analysis client, Seaxng search, user profile learner
- add new drizzle migration

Shared:
- extend database schema, add new config fields
2026-07-02 00:02:41 +07:00

7.5 KiB
Raw Blame History

Gateway Event Design — The Pulse of Discord

"Events are the heartbeat of a distributed system." — Martin Fowler


🎯 Filosofi Gateway Events

Discord Gateway adalah jantung event-driven BETE:

  1. Single source of truth — Events adalah satu-satunya cara data bergerak antar service
  2. At-least-once delivery — Event bisa terkirim lebih dari sekali (idempotent consumers)
  3. Schema evolution — Events punya versioning untuk backward compatibility
  4. Observable — Setiap event tercatat untuk debugging dan audit

📦 Event Schema

Envelope

interface GatewayEvent<T = unknown> {
  /** Event type identifier — lowercase, colon-separated */
  type: string;

  /** Event payload */
  data: T;

  /** ISO 8601 timestamp of when the event was created */
  timestamp: string;

  /** Unique event ID for deduplication */
  eventId: string;

  /** Source service name */
  source: 'discord-gateway';

  /** Event schema version */
  version: number;

  /** Optional correlation ID for tracing request flows */
  correlationId?: string;
}

Event Size Limits

Limit Value Notes
Max payload size 256KB Larger payloads → reference via URL
Max nesting depth 5 levels Prevent billion laughs attack
String max length 100KB Truncate with ... (truncated) suffix

📋 Event Catalog

Message Events

// discord:message:created
interface MessageCreatedEvent {
  id: string;
  channelId: string;
  guildId: string;
  author: {
    id: string;
    name: string;
    discriminator: string;
    avatar: string | null;
    isBot: boolean;
  };
  content: string;
  timestamp: string;    // ISO 8601
  editedTimestamp: string | null;
  attachments: AttachmentInfo[];
  replyTo?: string;     // Parent message ID
}

// discord:message:updated
interface MessageUpdatedEvent {
  id: string;
  channelId: string;
  content: string;
  editedTimestamp: string;
}

// discord:message:deleted
interface MessageDeletedEvent {
  id: string;
  channelId: string;
  guildId: string;
}

Analysis Events

// discord:message:analyzed
interface MessageAnalyzedEvent {
  messageId: string;
  status: 'pending' | 'complete' | 'error';
  severity: 'safe' | 'low' | 'medium' | 'high' | 'critical';
  categories: string[];
  confidence: number;           // 01
  summary: string;
  analyzedAt: string;
  processingTimeMs: number;
}

Voice Events

// discord:voice:started
interface VoiceStartedEvent {
  guildId: string;
  channelId: string;
  channelName: string;
  startedAt: string;
  participants: Array<{
    userId: string;
    userName: string;
  }>;
}

// discord:voice:stopped
interface VoiceStoppedEvent {
  guildId: string;
  channelId: string;
  duration: number;   // seconds
  segmentsCount: number;
}

// discord:voice:uploaded
interface VoiceUploadedEvent {
  segmentId: string;
  guildId: string;
  channelId: string;
  userId: string;
  userName: string;
  duration: number;
  fileUrl: string;
  fileSize: number;
  timestamp: string;
}

Attachment Events

// discord:attachment:created
interface AttachmentCreatedEvent {
  id: string;
  messageId: string;
  channelId: string;
  url: string;
  filename: string;
  contentType: string;
  size: number;
  width?: number;
  height?: number;
}

// discord:attachment:uploaded
interface AttachmentUploadedEvent {
  id: string;
  messageId: string;
  storageUrl: string;
  thumbnailUrl?: string;
  fileSize: number;
  processingTimeMs: number;
}

🔄 Event Lifecycle

┌──────────┐
│ Discord  │  (messageCreate, voiceStateUpdate, etc.)
└────┬─────┘
     │
     ↓
┌──────────────┐
│ Discord.js   │  (client events)
└────┬─────────┘
     │
     ↓
┌─────────────────────────────┐
│ Message Capture Controller  │  (messageCapture.ts)
│  - Parse event             │
│  - Store in database       │
│  - Publish to Redis        │
└────┬───────────────────────┘
     │
     ↓
┌─────────────────────┐
│ Redis Pub/Sub       │  (channel: discord:message:created)
└────┬────────────────┘
     │
     ├──────────────────────────────┐
     ↓                              ↓
┌──────────────────┐    ┌──────────────────┐
│ Backend Service  │    │ AI Moderation    │
│ - Index message  │    │ - Analyze text   │
│ - Store in DB    │    │ - Update status  │
│ - Broadcast WS   │    │ - Publish result │
└──────────────────┘    └──────────────────┘

🧪 Event Testing

// Helper untuk generate test events
function createTestEvent<T>(type: string, data: T): GatewayEvent<T> {
  return {
    type,
    data,
    timestamp: new Date().toISOString(),
    eventId: crypto.randomUUID(),
    source: 'discord-gateway',
    version: 1,
  };
}

describe('MessageCreatedEvent', () => {
  it('is properly formatted', () => {
    const event = createTestEvent('discord:message:created', {
      id: 'msg_1',
      channelId: 'ch_1',
      guildId: 'guild_1',
      author: { id: 'user_1', name: 'Test', discriminator: '0000', avatar: null, isBot: false },
      content: 'Hello world',
      timestamp: new Date().toISOString(),
      editedTimestamp: null,
      attachments: [],
    });

    expect(event.type).toBe('discord:message:created');
    expect(event.data.content).toBe('Hello world');
    expect(event.source).toBe('discord-gateway');
    expect(event.version).toBe(1);
  });
});

📊 Event Performance Metrics

Metric Target Alert
Processing latency <50ms p99 >200ms
Event throughput >1000/s <100/s (unusual)
Redis publish latency <5ms >20ms
Event loss rate 0% >0.01%
Queue depth <100 >1000

⚠️ Anti-Patterns Events

Processing-heavy event handlers

// ❌ JANGAN — blocking event loop
eventBus.on('message:created', async (event) => {
  const result = await expensiveAnalysis(event.data.content);
  await db.save(result);
  // Event handler for 100 msg/s = bottleneck
});

// ✅ Queue heavy work
eventBus.on('message:created', async (event) => {
  await analysisQueue.add(event);  // Worker processes async
});

Missing idempotency

// ❌ JANGAN — duplicate events create duplicate records
async function handleMessageCreated(event) {
  await db.insert({ id: event.data.id, content: event.data.content });
  // If event arrives twice → duplicate key error
}

// ✅ Idempotent: UPSERT
async function handleMessageCreated(event) {
  await db.upsert({ id: event.data.id }, { content: event.data.content });
}

🔗 Referensi

Sumber Konsep
Redis Pub/Sub Event backbone
CloudEvents Event schema standard
Discord Gateway Discord events

"Setiap event adalah denyut nadi — tanda bahwa sistem masih hidup dan berbicara." ❄️🩵