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ênwindow) 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:
| Type | Payload |
|---|---|
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ống | Xử lý |
|---|---|
| Mất mạng / Server lỗi | Auto-reconnect với exponential backoff (1s → 2s → 4s → ... max 30s) |
| Kết nối lại thành công | Reset backoff về 1s |
| Server im lặng > 5 phút | Heartbeat timeout → reconnect |
| User logout | isAuthenticated = false → đóng kết nối |
| Tắt tab / Chuyển trang | React 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 (nativeEventSourcekhông hỗ trợ custom headers)