2026-07-15-p2p-timeout-reconnect
⟦tag:v2|task|2026-07-15-p2p-timeout-reconnect⟧
kind: task
id: 2026-07-15-p2p-timeout-reconnect
lane: R8
title: P2P 5s 流拍 + 断线重连(Day 3 主,6h)
objective: 收口 W29 5 日计划 R8 lane——把 R7 落的实时竞价补成"流拍可重连",含 5s 流拍机制 + 客机/房主断线重连 + 房主转移 + 顶部条幅 UI。
scope: docs/plans/active/2026-07-13-5日开发计划-v1.md § 2, docs/design/active/联机P2P房间骨架-v1.md, src/data/multiplayer/**(只读 + 新增), src/utils/multiplayer/**(只新增), src/components/multiplayer-lobby/**(只新增)
acceptance:
src/utils/multiplayer/timeout.ts落盘,导出applyTimeout(snapshot, now): RealtimeBidSnapshot,出价后 5s 无新出价 →settled并广播给所有 seatsrc/utils/multiplayer/reconnect.ts落盘,导出 3 个纯函数 + 1 个 typeReconnectState:联合类型'online' | 'reconnecting' | 'recovered' | 'offline'applyDisconnect(roomState, seatId, now): RoomState—— 标记reconnecting,30s 内重连 →recovered;30s 后 →offlineapplyReconnect(roomState, seatId, now): RoomState—— 恢复 seat + bid 状态transferHost(roomState, now): RoomState—— 房主 30s 内不重连 → 转移给第一个在线 seat
- 单元测试:
timeout.regression.ts+reconnect.regression.ts5 场景全过 = ① 5s 流拍 ② 客机掉线 30s 重连 ③ 客机掉线 60s 不重连 ④ 房主掉线 30s 重连 ⑤ 房主掉线 60s 转移 src/components/multiplayer-lobby/ReconnectBanner.tsx落盘,顶部条幅:任一 seat 处于reconnecting时显「XX 正在重连…」+ 倒计时,30s 后变「XX 已离线」- 跟 R7 的
RealtimeBidPanel.tsx集成:settled触发回调时显「本轮拍卖结束」 cmd /c npm run build通过,npm run lint无新增错误src/data/auction/**/src/utils/auctionRules.ts/src/components/auction/**任何代码未被改动ReconnectState跟现有MultiplayerRoomState的RoomPhase平级不冲突src/App.tsx主导航未被改动(只动 multiplayer tab 入口,R10 lane 范围) current state: documented blockers: none next action: 主会话 7.15 上午按本卡 prompt 段派工,短工回执时按## 回执模板段格式 summary:- 这是 W29 5 日计划 R8 lane(2026-07-15 Day 3 主轴,6h 工作量),上接 R7 实时竞价(7.14 Day 2 收口),下接 R10 UI 集成(7.16 Day 4)。
- 5s 流拍跟
auctionRules.ts现有 5s 流拍语义独立,本卡不读auctionRules.ts,所有 timeout 走src/utils/multiplayer/timeout.ts。 - 不实现真实 WebSocket / 跨设备同步 / 历史出价回放(留 v1.4+)。
prompt(给短工 / 子会话的指令)
角色:[短工]#联机-P2P流拍重连@v1.0
lane:R8
时间窗:2026-07-15 上午 9:00 ~ 2026-07-15 下午 18:00(6h 工作量,单日)
承接文档:docs/plans/active/2026-07-13-5日开发计划-v1.md § 2
前置依赖:R7 实时竞价已完成(realtimeBidding.ts + RealtimeBidState 落盘)
任务清单(按顺序做)
-
timeout 层:
src/utils/multiplayer/timeout.ts- 导出
applyTimeout(snapshot: RealtimeBidSnapshot, now: number): RealtimeBidSnapshot - 规则:
state === 'bidding' || state === 'antiSnipe'且now > deadlineAt→ 返回state: 'settled', extends: snapshot.extends;其他态返回原 snapshot - 禁止
setTimeout/setInterval,接受now: number注入 - 5s 流拍 = R7 的
deadlineAt已经由 R7 算出,本函数只判断超时;R7 的 deadlineAt 是从placeBid算起的「最后出价 +5s」(注:R7 文档已隐含此语义,本卡不重新设计)
- 导出
-
reconnect 层:
src/utils/multiplayer/reconnect.ts- 导出
ReconnectStatetype:'online' | 'reconnecting' | 'recovered' | 'offline' - 导出
applyDisconnect(roomState, seatId, now): RoomState—— 30s 内重连窗口 =now + 30_000 - 导出
applyReconnect(roomState, seatId, now): RoomState—— 恢复 seat + bid 状态 - 导出
transferHost(roomState, now): RoomState—— 房主 30s 不重连 → 转移给seats.find(s => s.status === 'online' && s.id !== hostId)第一个 - 房主转移后,旧 host 标记
offline,新 host 标记'online' + isHost: true - 禁止 直接调
Date.now(),全部接受now: number注入
- 导出
-
UI 层:
src/components/multiplayer-lobby/ReconnectBanner.tsx- 接收
roomState: RoomState+now: numberprops - 顶部条幅:任一 seat
status === 'reconnecting'→ 显「{seatId} 正在重连…({countdown}s)」+ 倒计时 - 30s 后变「{seatId} 已离线」
- 房主转移时显「房主已转移给 {newHostId}」3 秒淡出
- 不接主导航
- 接收
-
集成:
RealtimeBidPanel.tsx集成applyTimeout- 当 snapshot.state 变成
settled时,触发onSettled(snapshot.currentBid, snapshot.currentBidder)回调 - UI 显「本轮拍卖结束,中标:{currentBidder} @ {currentBid}」
- 当 snapshot.state 变成
-
测试层
src/utils/multiplayer/timeout.regression.ts5 场景src/utils/multiplayer/reconnect.regression.ts5 场景- 用项目现有的
node --test或 vitest 跑通,输出 PASS/FAIL
-
验收
cmd /c npm run build通过cmd /c npm run lint无新增错误- 10 场景测试全过(timeout 5 + reconnect 5)
文件边界(强制)
- ✅ 新增:
src/utils/multiplayer/timeout.ts - ✅ 新增:
src/utils/multiplayer/timeout.regression.ts - ✅ 新增:
src/utils/multiplayer/reconnect.ts - ✅ 新增:
src/utils/multiplayer/reconnect.regression.ts - ✅ 新增:
src/components/multiplayer-lobby/ReconnectBanner.tsx - 🔧 修改:
src/components/multiplayer-lobby/RealtimeBidPanel.tsx(只动applyTimeout集成,不加新功能) - ❌ 不碰:
src/data/auction/**/src/utils/auctionRules.ts/src/components/auction/** - ❌ 不碰:
src/utils/multiplayer/realtimeBidding.ts(R7 lane 已收口,本卡不重写) - ❌ 不碰:
src/App.tsx主导航(R10 lane 范围)
不做(明示)
- ❌ 真实 WebSocket / WebRTC(留 v1.4+)
- ❌ 跨设备同步(留 Phase 7)
- ❌ 历史出价回放
- ❌ 房主转移的二次确认弹窗(留 W30 UI 升级)
- ❌ 重连后回放最后 N 秒出价历史(留 v1.4)
风险 + 缓解
- 5s 流拍跟
auctionRules.ts5s 流拍语义冲突 → 严格遵守文件边界,新目录平级实现,本卡全程不读auctionRules.ts - 房主转移竞态(多个 seat 同时在线时选哪个)→
transferHost按seats数组顺序选第一个online非原 host applyReconnect跟applyTimeout时间窗口交叉 →applyReconnect只在status === 'reconnecting'有效,其他态拒绝
回执模板(短工完成时按这个格式回报主会话)
[R8 #联机-P2P流拍重连@v1.0 回执]
- 状态:✅ 完成 / ⚠️ 部分完成 / ❌ 阻塞
- 新增文件:<列出 5 个新增文件实际路径>
- 修改文件:<列出 1 个修改文件实际路径>
- timeout 5 场景测试:<PASS/FAIL 列表>
- reconnect 5 场景测试:<PASS/FAIL 列表>
- `npm run build`:✅ / ❌
- `npm run lint`:无新增 / 新增 N 个
- 风险 / 阻塞:<如有,列出;如无,写 "无">
- 残余风险:<如有,列出;如无,写 "无">
- 建议给 R10 接力点:<一句话>
(End of R8 task card)