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 } from '@vppos/core/types';

const PaymentPanel = () => {
useRealtimeEvent(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(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(
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 schema payload — TypeScript type được suy ra tự động
export const REALTIME_PAYLOAD_SCHEMAS = {
// ...existing
[REALTIME_EVENT_TYPES.ORDER_CANCELLED]: z
.object({
order_id: z.number(),
reason: z.string(),
})
.loose(),
};

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)
├── utils/mfe-event.ts # Event bus (shared)

apps/container/src/
├── components/EventStreamInitializer.tsx # SSE connection manager

Dependencies​

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