2026-09-18 13:34:35 +08:00
|
|
|
|
// packages/contracts/src/realtime.ts
|
|
|
|
|
|
// WS 实时消息契约(P2,技术实施方案 §7):OpenAPI 不覆盖 WS 帧,本文件单独
|
|
|
|
|
|
// 维护,与 openapi.yaml 生成类型同属契约单一事实来源。消息一律为以 type 判别
|
|
|
|
|
|
// 的联合;新增/修改消息类型须同步 __tests__/realtime.test.ts 的穷尽断言。
|
|
|
|
|
|
|
|
|
|
|
|
/** 心跳间隔(毫秒):客户端周期发送 ping、服务端回 pong;超时无心跳的连接由服务端踢除。 */
|
|
|
|
|
|
export const HEARTBEAT_MS = 20000;
|
|
|
|
|
|
|
2026-09-18 17:51:55 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* ov 入口 WS 网关挂载路径:与 HTTP 同端口(ov 8081),升级请求按本路径路由
|
|
|
|
|
|
* (services/api realtime 网关装饰器与 e2e/viewer 客户端共用,单一来源)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const REALTIME_WS_PATH = '/v1/realtime';
|
|
|
|
|
|
|
2026-09-18 13:34:35 +08:00
|
|
|
|
// ── server → client ──
|
|
|
|
|
|
|
|
|
|
|
|
/** 照片发布:副本应用后向相册房间广播(ov 侧,下同)。 */
|
|
|
|
|
|
export interface PhotoPublishedMessage {
|
|
|
|
|
|
type: 'photo.published';
|
|
|
|
|
|
photoId: string;
|
|
|
|
|
|
albumId: string;
|
|
|
|
|
|
publishedAt: string; // ISO UTC
|
|
|
|
|
|
entityVersion: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 照片隐藏。 */
|
|
|
|
|
|
export interface PhotoHiddenMessage {
|
|
|
|
|
|
type: 'photo.hidden';
|
|
|
|
|
|
photoId: string;
|
|
|
|
|
|
albumId: string;
|
|
|
|
|
|
entityVersion: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 照片删除(软删除的公开语义等同隐藏,客户端行为一致:即刻移出列表)。 */
|
|
|
|
|
|
export interface PhotoDeletedMessage {
|
|
|
|
|
|
type: 'photo.deleted';
|
|
|
|
|
|
photoId: string;
|
|
|
|
|
|
albumId: string;
|
|
|
|
|
|
entityVersion: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 点赞合并广播:短窗内同照片批次合并为一次 burst;originBatchIds 供客户端过滤自身批次。 */
|
|
|
|
|
|
export interface LikesBurstMessage {
|
|
|
|
|
|
type: 'likes.burst';
|
|
|
|
|
|
photoId: string;
|
|
|
|
|
|
albumId: string;
|
|
|
|
|
|
delta: number;
|
|
|
|
|
|
total: number; // 服务端权威累计
|
|
|
|
|
|
countVersion: number;
|
|
|
|
|
|
burstId: string;
|
|
|
|
|
|
originBatchIds: string[]; // 合并进本 burst 的批次 ID(含己则抑制自身动画)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 相册浏览数更新。 */
|
|
|
|
|
|
export interface StatsUpdatedMessage {
|
|
|
|
|
|
type: 'stats.updated';
|
|
|
|
|
|
albumId: string;
|
|
|
|
|
|
viewCount: number;
|
|
|
|
|
|
countVersion: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 心跳应答:回显客户端 ping 携带的 ts。 */
|
|
|
|
|
|
export interface PongMessage {
|
|
|
|
|
|
type: 'pong';
|
|
|
|
|
|
ts: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 17:51:55 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 错误帧:客户端消息无法处理时的服务端应答(如 subscribe 的相册不存在/
|
|
|
|
|
|
* 不公开)。连接保持打开——客户端可修正后重发(如改订其它相册或等待恢复)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface RealtimeErrorMessage {
|
|
|
|
|
|
type: 'error';
|
|
|
|
|
|
/** 契约 ErrorCodes 域码(subscribe 校验失败 → ALBUM_NOT_PUBLIC 等)。 */
|
|
|
|
|
|
code: string;
|
|
|
|
|
|
/** 可读说明(可选,不承诺稳定性,客户端只应消费 code)。 */
|
|
|
|
|
|
message?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:34:35 +08:00
|
|
|
|
export type ServerMessage =
|
|
|
|
|
|
| PhotoPublishedMessage
|
|
|
|
|
|
| PhotoHiddenMessage
|
|
|
|
|
|
| PhotoDeletedMessage
|
|
|
|
|
|
| LikesBurstMessage
|
|
|
|
|
|
| StatsUpdatedMessage
|
2026-09-18 17:51:55 +08:00
|
|
|
|
| PongMessage
|
|
|
|
|
|
| RealtimeErrorMessage;
|
2026-09-18 13:34:35 +08:00
|
|
|
|
|
|
|
|
|
|
// ── client → server ──
|
|
|
|
|
|
|
|
|
|
|
|
/** 订阅相册房间;服务端校验相册公开后加入。 */
|
|
|
|
|
|
export interface SubscribeMessage {
|
|
|
|
|
|
type: 'subscribe';
|
|
|
|
|
|
albumToken: string;
|
|
|
|
|
|
sinceVersion?: number; // 缺省从订阅时刻起推送,不补播历史
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 退订相册房间。 */
|
|
|
|
|
|
export interface UnsubscribeMessage {
|
|
|
|
|
|
type: 'unsubscribe';
|
|
|
|
|
|
albumToken: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 心跳;服务端原样回 pong。 */
|
|
|
|
|
|
export interface PingMessage {
|
|
|
|
|
|
type: 'ping';
|
|
|
|
|
|
ts: number; // epoch 毫秒
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type ClientMessage = SubscribeMessage | UnsubscribeMessage | PingMessage;
|