Files
photo-live/apps/viewer/src/realtime/router.ts
T

59 lines
2.6 KiB
TypeScript
Raw Normal View History

// apps/viewer/src/realtime/router.ts —— 服务端消息路由(P2 Task 10)。
// 纯函数 + 依赖注入:帧语义集中一处,单测直测(自身过滤/隐藏暂停/减少动画)。
//
// likes.burst 自身过滤(A21 客户端侧):originBatchIds 与本会话已提交批次求交
// 得己方 delta——己方贡献不播气泡(点击时已见过动画),他人贡献照常;total
// 恒为权威值,无论过滤与否都校准。页面不可见(visibilitychange)或「减少动画」
// 开启时跳过气泡(队列即弃——回台只看得到校准后的数字),计数校准不受影响。
import type { ServerMessage } from '@photo/contracts';
export interface RealtimeRouterDeps {
/** 页面当前是否不可见(document.hidden)。 */
isHidden(): boolean;
/** 「减少动画」偏好是否开启。 */
reducedMotion(): boolean;
/** originBatchIds 中属于本会话已提交批次的 delta 合计。 */
ownDeltaIn(batchIds: string[]): number;
/** 点赞计数权威校准(版本化)。 */
calibrateLike(photoId: string, total: number, countVersion: number): void;
/** 他人点赞气泡(photoId + 他人贡献 delta)。 */
showBubble(photoId: string, delta: number): void;
/** 新照片到达:亮提示条(不自动插入)。 */
notifyNewPhotos(): void;
/** 照片隐藏/删除:移出序列(调用方负责大图收口与提示)。 */
removePhoto(photoId: string): void;
/** 相册浏览数更新(stats.updated)。 */
setViewCount(viewCount: number): void;
}
export function handleServerMessage(message: ServerMessage, deps: RealtimeRouterDeps): void {
switch (message.type) {
case 'pong':
return; // 心跳应答:无动作
case 'error':
// subscribe 校验失败等:连接保持,服务端已说明——本地备注即可。
console.info(`[viewer] realtime error frame: ${message.code}`);
return;
case 'photo.published':
deps.notifyNewPhotos();
return;
case 'photo.hidden':
case 'photo.deleted':
deps.removePhoto(message.photoId);
return;
case 'likes.burst': {
const own = deps.ownDeltaIn(message.originBatchIds);
// 校准无条件执行(含纯己方 burst——批次响应丢失时这是唯一权威回执)。
deps.calibrateLike(message.photoId, message.total, message.countVersion);
const others = message.delta - own;
if (others > 0 && !deps.isHidden() && !deps.reducedMotion()) {
deps.showBubble(message.photoId, others);
}
return;
}
case 'stats.updated':
deps.setViewCount(message.viewCount);
return;
}
}