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:
78
services/msg/src/shared/push/redis-publisher.ts
Normal file
78
services/msg/src/shared/push/redis-publisher.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { getRedis } from "../redis/redis.client.js";
|
||||
import { logger } from "../observability/logger.js";
|
||||
|
||||
/**
|
||||
* Redis Pub/Sub 实时推送发布器(M7 / ADR-040)。
|
||||
*
|
||||
* 仲裁依据:
|
||||
* - M7:msg 服务消费 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user