feat(push-gateway,msg): redis pubsub backplane for real-time notifications

M7: ADR-040 Redis Pub/Sub as state routing backplane

- push-gateway: remove Kafka consumer, add SSE endpoint

- SSE: subscribe to Redis user:{userId}:notify on connect

- msg: publish notifications to Redis Pub/Sub instead of HTTP push

- docker-compose: remove Kafka env from push-gateway
This commit is contained in:
SpecialX
2026-07-15 01:28:55 +08:00
parent a75527be80
commit 6af1aa0d82
14 changed files with 402 additions and 484 deletions

View File

@@ -0,0 +1,78 @@
import { getRedis } from "../redis/redis.client.js";
import { logger } from "../observability/logger.js";
/**
* Redis Pub/Sub 实时推送发布器M7 / ADR-040
*
* 仲裁依据:
* - M7msg 服务消费 Kafka持久化、脱敏通过 Redis Pub/Sub 发布实时推送
* - 通道命名:`user:{userId}:notify`
* - realtime-gateway 实例仅在用户连接 SSE 时 SUBSCRIBE 该通道
* - 软失败Redis 不可用时返回 { sent: false },不阻断主流程
*
* 消息格式JSON 字符串,由 realtime-gateway SSE 端点原样转发为 `data: {json}\n\n`
*/
export interface RedisPushRequest {
userId: string;
event: string;
data: Record<string, unknown>;
}
export interface RedisPushResult {
sent: boolean;
error?: string;
}
/**
* 构造 Redis Pub/Sub 通道名。
* 通道命名规范M7`user:{userId}:notify`
*/
export function notifyChannel(userId: string): string {
return `user:${userId}:notify`;
}
/**
* 通过 Redis Pub/Sub 发布实时通知到用户的通道。
*
* 软失败语义:
* - REDIS_URL 未配置 → 返回 { sent: false },不报错
* - Redis 发布错误 → 返回 { sent: false, error }logger.warn
* - 成功 → 返回 { sent: true }
*
* 消息体为 JSON 字符串realtime-gateway SSE 端点原样转发为 SSE data 帧。
*/
export async function publishToRedisPubSub(
req: RedisPushRequest,
): Promise<RedisPushResult> {
const client = getRedis();
if (!client) {
return { sent: false };
}
const channel = notifyChannel(req.userId);
const payload = JSON.stringify({
event: req.event,
data: req.data,
timestamp: new Date().toISOString(),
});
try {
const receivers = await client.publish(channel, payload);
if (receivers === 0) {
// 无订阅者:用户未通过 SSE 连接,不算错误(站内信仍可见)
logger.debug(
{ userId: req.userId, channel },
"Redis Pub/Sub: no active SSE subscribers (user offline)",
);
}
return { sent: true };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
logger.warn(
{ err, userId: req.userId, channel },
"Redis Pub/Sub publish failed",
);
return { sent: false, error };
}
}