Relay · 即時事件
即時事件與重新連線復原
WebSocket 負責傳遞即時對話事件。事件延遲、重新連線後重複送達,或與 REST 補齊結果混合時,channel-local seq 仍是排序依據。
建立連線
連到 Phoenix socket endpoint,並傳入 REST API 使用的同一個 host-signed JWT:
import { Socket } from "phoenix";
const socket = new Socket("wss://relay.example.com/socket", {
params: { token: relayToken }
});
socket.connect(); Server 先驗證 token,client 才能加入任何 channel。
加入 channel
Topic 包含已簽章的 tenant ID 與 channel ID:
tonetify-relay:<tenant_id>:<channel_id> 傳入 client 已經套用的最後一個 sequence:
const channel = socket.channel(
`tonetify-relay:${tenantId}:${channelId}`,
{ last_seq: lastAppliedSeq }
);
channel.join(); Relay 在 join 時檢查兩個條件:
- Topic tenant 必須等於 token 的
tenant_id。 - Token 的
user_id必須是 channel member。
Membership 或 tenant 檢查失敗時回傳 not_a_member。Join 超過設定的 frame budget 時回傳 rate_limited。
Join 後的事件
Join 成功後,Relay 會傳送目前的 presence state、要求補齊的 message,以及 channel unread count。
| 事件 | 意義 |
|---|---|
presence_state | Channel 目前的 presence snapshot |
presence_diff | Snapshot 之後的 presence change |
msg:new | 即時 message,或 catch-up window 內的一則 message |
catch_up:truncated | 這個 catch-up window 之後仍有 message |
unread_count | 這個 user 與 channel 目前的 unread count |
這五種事件構成契約測試版的穩定即時核心。Edit、retract、pin、reaction、thread 與 typing event 目前屬於預覽介面。它們已存在於實作與 SDK,但事件名稱、payload 與恢復行為尚未納入公開相容性政策。
Catch-up window
Join payload 包含 last_seq 時,Relay 會讀取 sequence 大於該值的 message,再依升冪逐筆 push msg:new。WebSocket catch-up window 預設最多 200 則 message。
如果仍有後續 message,Relay 會在這批資料之後傳送以下事件:
{
"channel_id": "channel-uuid",
"after_seq": 42,
"next_after_seq": 242,
"limit": 200
} catch_up:truncated 表示這批資料結束於 next_after_seq,不代表 Relay 遺失 message。
使用新的 last_seq 再次 join,或透過 REST 從該 sequence 繼續:
GET /api/channels/{channel_id}/messages?after_seq=242&limit=100
Authorization: Bearer <jwt> REST 每次最多回傳 100 則 message。持續請求,直到 response 少於 100 則。
重新連線演算法
可靠的 client 應遵循以下順序:
- 依 channel 保存已完整套用的最大
seq。 - 開始復原前,先訂閱即時
msg:newevent。 - 使用保存的 sequence join。
- 依 message
id合併 catch-up 與即時 message。 - 依
seq顯示 message,不依 arrival order。 - Catch-up 被截斷時,從
next_after_seq繼續,直到補完缺口。 - Message 進入 local store 後,才更新保存的 sequence。
Catch-up message 可能與 local store 內的 message 重複。復原期間允許 duplicate delivery;穩定的 message id 與 seq 讓 merge result 保持一致。
斷線期間傳送
Send timeout 代表 write result 不確定。連線恢復後,保留原 nonce 並重試。如果第一次 attempt 已經 commit,Relay 會回傳既有 message。
不要只因 transport disconnected 就產生新 nonce。新 nonce 代表新的 logical message。
目前的 SDK 邊界
目前的 @tonetify/relay 提供:
- 使用 exponential backoff 與 jitter 重新連線 socket
- 預設 30 秒 heartbeat
- 透過
channel.join(lastSeq)執行 catch-up join - 使用
MessageStore排序並抑制 duplicate - 使用
CatchUpManager在 join 前讀取 store 最新的 sequence catch_up:truncatedevent
有兩個復原步驟尚未自動完成:
- In-memory
MessageStore不會跨 page reload 或 process restart 保存最後的 sequence。 - Socket 自動 rejoin 不會把 join parameter 更新為最新套用的 sequence,
CatchUpManager也不會自動排空 truncated window。
在這些缺口補齊前,host 應保存 last_seq,並在 socket 重新連線或 application 再次進入 active state 時執行 REST catch-up。先依 id 合併 REST 與 WebSocket message,再以 seq 排序。
這是 SDK productization gap。Server reconnect contract 已支援完整復原。