AskaEth df9fc85f76 feat(chat): 普通发送未连接时进队列(不再回滚),连接恢复后按发送顺序自动补发
主人 2026-10-05 裁决:普通发送在「未连接」时进队列。

此前 deliver 拿到 sent=false 就 rollback:气泡摘掉、正文退回输入栏,外加一句「还没连上,
这条没发出去,请稍后手动重试」。内容是没丢,但把「网络还没好」说成了一次要用户自己负责的
失败,而且他还得对着输入栏再发一遍。这一批按 W38「分享落地等连接」那套语义重做普通发送:
不报错、内容不丢、连接恢复后自动补发。W38 的落地时机、PendingShareHolder.take() 的原子语义、
ChatSocket 的连接状态机一个字都没动。

① 三档处置:未就绪就排队(新纯函数 wsSendOutcome,ui/chat/SendQueue.kt)
判据的输入只有两个:sendChat 的回值,以及这一刻 socket 自己的连接状态。sent=true ⇒ Dispatched
(帧已经交给 socket,成功路径一个字没动:不打标记、不说告知、不写任何状态);sent=false 且
连接就绪 ⇒ Failed(连接是好的,那 false 只可能来自 socket 对象已空 / 已被 cancel / 序列化抛
异常,「发送失败」在这一刻是事实 ⇒ 走今天那条 rollback + SEND_FAILED_MESSAGE);sent=false 且
未就绪 ⇒ Queued(ChatSocket.send 第一行就按状态挡掉,帧连序列化都没做、服务端一个字节都收不到,
所以既不回滚也不进 error 横幅)。未就绪四档(Idle / Connecting / Reconnecting / Offline)
一视同仁:主人的原话是「未连接就进队列」,Offline 只是不会自己变好,内容仍然一个字节都不会丢,
两档的差别只在告知措辞(这一点与 W39 给分享落地定的规矩一致)。

② 队列与补发:顺序与「只发一次」的保证点
deliver 的 WS 分支只改了「未就绪」这一支:queueForConnection 把这条留在待发态
(ChatUiState.queuedForConnection,条目只有 localId 与投递那一刻定下的并行声明;正文 / 图片 /
附件仍从那条气泡上现取,同一件事不留第二份真相源,也就不存在两条真相分叉)。触发点只有一个:
socket 状态变成 Connected 那一刻(onConnectionChanged,由状态流驱动、没有任何轮询),外加
入队之后的一次竞态兜底(连接恰好在投递与入队之间就绪时,那一次补发会扑空)。补发从队头开始,
FIFO 顺序 = 用户发送的先后顺序。
「只发一次」的保证点是「队头 + 出队恰好一次」:队头只在这一步有了结论(发出去了 / 真失败 /
气泡已经不在了)之后才被 dropQueuedSend 摘掉,所以重复的触发拿到的是同一个键、不会多出第二次
出队;flushQueuedSends 是同步函数(一个挂起点都没有),两个触发点在主线程上不可能交错。
第二道保证在服务端:补发复用同一个 clientMsgId(绝不铸第二个),真重复到达也只会被回
chat_duplicate,不会落成第二条消息。补发又失败时按今天的失败路径处理(回滚 + 文案),
绝不静默吞掉。已知边界(写在 KDoc 里):打断声明不随补发重放——补发时在跑的那一轮早就不是
用户按发送时看到的那一个了,隔半分钟再去打断可能毁掉他没见过的一轮。

③ 状态表达:仍是六态,③ 里多一句更准的话
不新增第七态:等连接的这条走六态里的 ③ 排队中(phaseOf 的判据从「收到过入队回执」扩为
「收到过回执,或本端因未连接把它留住了」),两类排队的区别由气泡小标说清——新纯函数
pendingBubbleFooter 给 ③ 加四个字:「排队中 · 还没连上」。为什么挂在气泡上而不只是那句告知:
告知八秒就收走,而这是个会持续几十秒的状态,牌子撤掉之后光看「排队中」分不出是「她已经收下、
前面还排着队」还是「连都没连上、一个字节都没上路」。柔和告知走新通道 ChatUiState.sendQueueNotice
(与 shareNotice 分开:两条描述的是不同的动作,可以同时挂着),文案 SEND_QUEUED_NOTICE 是
「还没连上,这条排队等着,连上后自动发出」;Offline 那一档换 SEND_QUEUED_OFFLINE_NOTICE
「现在处于离线,需要重新登录或检查服务器地址;这条还没发出去,已排队不会丢」——不提「连上」
「自动发」,那一档没有会自动到来的连接。不刷屏:一轮「队列从空到空」只说一次(连续几条离线
消息不会每按一次发送就弹一遍),记账在 queueForConnection。

④ 未覆盖:REST 通道(长文 / 图片 / 附件)保持今天的行为
POST /api/v1/chat 那条路仍是失败回滚 + 既有网络错误文案,本批不动。两条理由:一是它给不出
「未连接」这个信号(拿到的只是一次网络错误,而请求可能已被服务端收到、也可能没有),
判据不可靠;二是要补发就得重跑图片重编码与附件上传换存储名,而本地 URI 的读授权可能已经失效。
代价:未连接时发长文 / 图片 / 附件仍要用户手动重发一次;影响面仅这一条通道,WS 那条(短文本,
日常绝大多数消息)已经覆盖。

⑤ 未连接时不再有的那句话
「还没连上,这条没发出去,请稍后手动重试」(W39 加的 SEND_NOT_CONNECTED_MESSAGE)与它的判据
sendFailureMessage 一并删除:回滚那条路现在只在连接就绪时才走,这句话已经没有出口,留着就是
一段描述旧行为的死文案。SendFailureMessageTest 的断言改成新事实(未就绪 ⇒ 排队,而不是回滚
加那句话)并连同新的队列判据并入 SendQueueTest,一条断言都没删。另把「空输入什么都不发生」
这条边界抽成纯函数 hasSendableContent,输入栏的 send 与投递口的 emitAndDeliver 共用同一条判据
(行为不变,只是原本写在两处的同一个 if 现在可单测,也不会再分叉)。

测试:新增 SendQueueTest(21 条:三档判据穷举 WsState.entries、两档告知措辞、队列顺序与出队
恰好一次、气泡小标与六态、空输入边界)。assembleDebug 成功;testDebugUnitTest 1118 条
(1101 减 4 加 21),failures 0 / errors 0。
2026-10-05 13:57:54 +08:00
S
Description
昔涟在安卓设备上的载体。
Apache-2.0
19 MiB
0 Stars 1 Watchers 0 Forks
2026-05-29 22:15:56 +08:00