Chuyển tới nội dung chính

Real-time Events (SSE)

Hệ thống sử dụng Server-Sent Events (SSE) để nhận real-time notifications từ backend — ví dụ: đơn hàng mới, thanh toán thành công, thông báo hệ thống.

Kiến trúc

Backend SSE ──→ Container (EventStreamInitializer) ──→ mfeEventBus (CustomEvent) ──→ MFE apps
│ │
│ 1 kết nối duy nhất │ broadcast toàn cục
│ auto-reconnect │ qua window.dispatchEvent
  • Container giữ 1 kết nối SSE duy nhất tới /api/v1/events/stream
  • Khi nhận event → broadcast qua mfeEventBus (cùng cơ chế CustomEvent đã dùng cho store sync, auth, navigation)
  • MFE subscribe bằng hook useRealtimeEvent() từ @vppos/core/hooks — không biết gì về SSE

Tại sao không dùng RTK Query?

SSE là kết nối giữ mở liên tục (server push liên tục), khác với REST API (request → response 1 lần). RTK Query không hỗ trợ mô hình stream.

Tại sao không mỗi MFE tự kết nối?

  • Lãng phí: N MFE = N kết nối tới cùng 1 endpoint
  • Mỗi MFE có Redux store riêng biệt, không thể share state trực tiếp
  • mfeEventBus (CustomEvent trên window) là cầu nối duy nhất đã proven trong codebase

Event Types

Định nghĩa tập trung tại core/src/types/realtime.types.ts:

export const REALTIME_EVENT_TYPES = {
ORDER_CREATED: "order_created",
ORDER_PAID: "order_paid",
NOTIFY: "notify",
} as const;

Mỗi type có typed payload riêng:

TypePayload
order_created{ company_id, order_id, invoice_number, message }
order_paid{ company_id, order_id, invoice_number, message, total }
notify{ message, ...extras }

Cách sử dụng trong MFE

Cơ bản

import { useRealtimeEvent } from '@vppos/core/hooks';
import { REALTIME_EVENT_TYPES, type OrderPaidPayload } from '@vppos/core/types';

const PaymentPanel = () => {
useRealtimeEvent<OrderPaidPayload>(REALTIME_EVENT_TYPES.ORDER_PAID, (data) => {
toastUtils.success(data.message);
});

return <div>...</div>;
};

Lọc theo điều kiện (ví dụ: đúng đơn hàng đang xử lý)

const currentOrderId = 2398;

useRealtimeEvent<OrderPaidPayload>(REALTIME_EVENT_TYPES.ORDER_PAID, (data) => {
if (data.order_id === currentOrderId) {
toastUtils.success(`Đã nhận ${data.total.toLocaleString()}đ`);
handleGoToInvoice();
}
});

Bật/tắt theo điều kiện

const isWaitingPayment = order?.status === 'PENDING';

// Chỉ subscribe khi đang chờ thanh toán
useRealtimeEvent<OrderPaidPayload>(
REALTIME_EVENT_TYPES.ORDER_PAID,
(data) => { ... },
isWaitingPayment, // enabled flag
);

Thêm event type mới

Khi backend thêm event type mới, chỉ cần sửa 1 file trong core:

core/src/types/realtime.types.ts
// 1. Thêm key vào REALTIME_EVENT_TYPES
export const REALTIME_EVENT_TYPES = {
ORDER_CREATED: "order_created",
ORDER_PAID: "order_paid",
ORDER_CANCELLED: "order_cancelled", // ← mới
NOTIFY: "notify",
} as const;

// 2. Thêm payload interface
export interface OrderCancelledPayload {
order_id: number;
reason: string;
}

// 3. Thêm vào RealtimePayloadMap
export interface RealtimePayloadMap {
// ...existing
[REALTIME_EVENT_TYPES.ORDER_CANCELLED]: OrderCancelledPayload;
}

Không cần sửa EventStreamInitializer hay mfeEventBus — chúng broadcast tất cả event types.

Cơ chế kết nối

Tình huốngXử lý
Mất mạng / Server lỗiAuto-reconnect với exponential backoff (1s → 2s → 4s → ... max 30s)
Kết nối lại thành côngReset backoff về 1s
Server im lặng > 5 phútHeartbeat timeout → reconnect
User logoutisAuthenticated = false → đóng kết nối
Tắt tab / Chuyển trangReact cleanup → đóng kết nối

Cấu trúc file

core/src/
├── types/realtime.types.ts # Event types & payloads (shared)
├── hooks/useRealtimeEvent.ts # Hook subscribe event (shared)

apps/container/src/
├── components/EventStreamInitializer.tsx # SSE connection manager
├── utils/mfeEventBus.ts # Event bus (đã có sẵn, thêm SSE types)

Dependencies

  • event-source-polyfill — Chỉ dùng trong container, để gửi Authorization header qua SSE (native EventSource không hỗ trợ custom headers)