docs(HH-566): define realtime replay contract (#140)

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 23:47:43 +08:00
committed by GitHub
co-authored by rogee
parent f36606a4f2
commit e1ae9b57d8
@@ -137,6 +137,38 @@ ActionCableListener.OnEvent (internal/wsevent/bridge_listener.go:51)
└── 前端 Vue SPA WebSocket 连接接收 → Vuex/Pinia store 更新 → UI 刷新
```
#### Durable realtime 投递语义
配置 WorkerPool 后,`EventPublisher` 先把 account 与 pubsub token 目标分别写入
`background_jobs`,再由 `realtime:event_publish` job 发布到 Redis、Hub 和 SSE。
幂等键避免同一条 `message.created`/目标重复入队,job claim 避免并发执行同一条
记录;它们不覆盖发布副作用与 job 完成状态之间的崩溃窗口:
1. job 已完成 Redis 发布,并可能已完成本地 SSE 投递;
2. 进程在将 `background_jobs.status` 更新为 `completed` 前退出;
3. stale-job 恢复将该记录重新置为可执行并再次发布。
因此,对外契约是 **durable at-least-once publication**,不是 exactly-once
delivery。进程恢复后,在线 WebSocket/SSE 消费者可能再次收到同一事件;account
与 pubsub token 是独立 job,重试和到达顺序也彼此独立。Redis Pub/Sub、Hub 和
SSE 不保存离线或慢消费者的确认状态,所以该契约保证发布尝试可恢复,不保证每个
客户端至少接收一次。
消费者必须把 realtime 事件作为可重放通知处理:
- `message.created` 使用订阅作用域、事件名和 payload 的稳定 message `id` 去重或
upsert;不得使用未出现在 wire payload 中的 background job ID。
- `*.updated` 按资源 `id` 幂等 upsert,并在 payload 提供 `updated_at`/版本时拒绝
旧更新;不要仅按资源 ID 永久丢弃后续合法更新。
- 重连或发现序列缺口时,以 REST API 返回的持久化资源为准。会触发声音、通知、
SDK callback 或其他非幂等副作用的外部消费者必须在执行副作用前自行去重。
当前风险可接受:复用的 dashboard 会按 message ID 替换重复消息,并按
conversation `updated_at` 忽略旧更新;widget 消息仓库也按 message ID upsert。
这与上游 Chatwoot 的异步 `ActionCableBroadcastJob` 行为一致。只有当业务要求
跨进程崩溃的 exactly-once 副作用时,才应另行引入稳定 transport event ID 与
消费端 inbox/ack;当前协议不承诺该能力。
### 2.5 通知创建
```