DSH / Atlas
2026-08-28implementedfeaturearchived 2026-09-04

Web connection recovery control

Web 连接恢复控件

The Web Client automatically rebuilt its Remote event generation and physical WebSocket after a failure, but the page exposed neither the outage nor a user recovery action. Its logical-generation and physical-socket retry loops could also drift: a `retry #N` message could describe another logical generation while the browser still waited on the same physical connection candidate. The Host sent an idle WebSocket Ping

English

Problem

The Web Client automatically rebuilt its Remote event generation and physical WebSocket after a failure, but the page exposed neither the outage nor a user recovery action. Its logical-generation and physical-socket retry loops could also drift: a retry #N message could describe another logical generation while the browser still waited on the same physical connection candidate. The Host sent an idle WebSocket Ping only every 30 seconds, and a user could not request a fresh attempt after restoring the Host or network.

Decision

The Host sends WebSocket Ping control frames every two seconds by default through the existing validated websocketHeartbeatIntervalMs configuration. Before each Ping it marks the socket as awaiting Pong; a socket still awaiting Pong at the next interval is terminated. ConnectionController is the sole retry scheduler. Online transport failures enter jittered exponential backoff whose cap starts at 500ms, doubles through 1s, 2s, 4s, and 8s, and stops growing at 10s; the actual delay is 50–100% of the cap. The continuous recovery decision supersedes this note's finite retry policy: attempts continue at the capped interval, and handshake readiness has a hard deadline. Each physical retry publishes connecting, writes one retry #N warning, asks Gateway mux to replace any candidate or active socket exactly once, and reopens the internal $events stream.

The Client Connection service exposes the identity-stable ctx.connection.state observable and ctx.connection.reconnect(). Its snapshot is undefined until the first connection outcome, then carries disconnected, connecting, or connected; equivalent states do not notify. Manual reconnect interrupts the current generation or retry delay, resets the attempt number, and starts retry 1 immediately through the same physical and logical path as automatic recovery. The browser's offline event immediately aborts active connection work, publishes disconnected, and suspends automatic retries. The next online transition publishes connecting, resets the attempt number, and starts again at the 500ms backoff tier; duplicate events do not create another loop. A fresh $events ready frame, rather than navigator.onLine, proves Host connectivity. Logical streams continue to own their baseline, cursor, and replay semantics after the replacement generation.

The Web Client architecture, Remote event delivery, and Session event transport retain their broader ownership decisions; this note supersedes only their former retry timing.

The Settings shell is a recovery-specific consumer and therefore injects Connection directly; ordinary feature code continues to use ctx.remote. Its private hooks compartment binds the state observable and reconnect command. The expanded sidebar renders ConnectionIndicator immediately to the right of Settings: disconnected is a pale-yellow Disconnected action, connecting displays Reconnecting in yellow while one to three dots advance every 500ms independently of retry timing, and a recovered connection displays pale-green Connected for two seconds. Hover or keyboard focus on either yellow state changes only the text to Reconnect now; press feedback uses a small warning-color transition, and no native title tooltip is present. Every visible state reserves the widest localized label and uses fixed icon and left-aligned text columns, so state changes do not move or resize the control. Initial startup and uninterrupted healthy operation render nothing.

Alternatives considered

Retry every two seconds without a terminal state. Rejected because a long outage would create continuous connection traffic. The retained exponential policy retries quickly at first, becomes progressively quieter, and retains the recovery action during retries. The continuous recovery decision governs retries after the cap is reached.

Render a full-width ConnectionBanner at the top of the viewport. Rejected because the status belongs beside the recovery action the user named, and a global overlay consumes unrelated page chrome. The primitive is the inline ConnectionIndicator; no ConnectionBanner compatibility export exists before the first tagged release.

Expose lifecycle control through ctx.remote.$connection. Rejected because retry state and commands belong to the Connection service rather than the Remote method namespace. Direct ctx.connection use remains exceptional and is appropriate here because the indicator itself controls reconnection.

Retry only when the user clicks. Rejected because recovery must remain automatic when the user is not watching the page; the button resets the backoff and bypasses its current wait.

Consequences

Idle browser connections generate more frequent heartbeat traffic than the former default, while long outages retain capped connection attempts under the continuous recovery decision. Deployments may override the Host Ping interval. Gateway mux owns no second retry timer, so every retry #N warning corresponds to one Controller-requested physical attempt.

A manual reconnect intentionally disrupts every logical Remote stream sharing the physical socket. Their existing generation supervisors restore state through fresh baselines or cursors, and one-way notifications remain non-replayed.

The connection state and browser-network input stay in the React-free transport layer. The Settings component receives a framework-bound selector hook and a plain callback, so no UI store duplicates transport state; only the two-second success presentation and 500ms dot animation are presentation-local.

Testing

Connection and Gateway tests pin the two-second heartbeat and Pong deadline, exponential retry caps and logs, browser offline suspension and online reset, manual sequence reset, one socket replacement per requested attempt, state deduplication, listener isolation, and disposal. Component tests pin healthy-state absence, hover/action copy, the independent dot animation, click behavior, and the two-second success state. The assembled Web test drives browser offline/online transitions, failed WebSocket attempts, stable indicator geometry, manual recovery, and the success confirmation through the shipped application.

中文

Problem

Web Client 会在故障后自动重建 Remote event generation 与物理 WebSocket,但页面既不显示断联,也不提供用户恢复操作。logical generation 与 physical socket 的重试循环还可能错位:retry #N 消息可能描述另一个 logical generation,而浏览器仍在等待同一个物理连接候选。Host 每 30 秒才发送一次空闲 WebSocket Ping,用户在恢复 Host 或网络后也无法主动要求一次全新尝试。

Decision

Host 默认通过既有且经过校验的 websocketHeartbeatIntervalMs 配置,每 2 秒发送一次 WebSocket Ping 控制帧。每次 Ping 前,它把 socket 标记为等待 Pong;到下一间隔仍未收到 Pong 的 socket 会被终止。ConnectionController 是唯一的 retry 调度器。在线状态下的传输失败进入带抖动的指数退避:上限从 500ms 开始,依次翻倍为 1s、2s、4s、8s,最终封顶 10s;实际延迟是上限的 50%–100%。持续恢复决策取代本笔记的有限重试策略:达到间隔上限后仍继续尝试,握手就绪设有硬期限。每次物理 retry 都发布 connecting、写一条 retry #N warning、要求 Gateway mux 恰好一次替换候选或活动 socket,再重开内部 $events stream。

Client Connection 服务暴露 identity 稳定的 ctx.connection.state observable 与 ctx.connection.reconnect()。snapshot 在首次连接结果前为 undefined,此后为 disconnected、connecting 或 connected;等价状态不触发通知。手动重连会中断当前 generation 或重试等待、重置 attempt 序号,并通过与自动恢复相同的物理和逻辑路径立即开始 retry 1。浏览器的 offline 事件会立即中断活动连接工作、发布 disconnected 并暂停自动 retry;下一次 online 转换会发布 connecting、重置 attempt 序号,并从 500ms 退避档重新开始;重复事件不会创建另一条循环。Host 是否可达由新的 $events ready 帧证明,而不是由 navigator.onLine 证明。替换 generation 建立后,各 logical stream 仍自行持有 baseline、cursor 与 replay 语义。

Web Client 架构、Remote 事件投递和会话事件传输继续持有各自更宽的所有权决策;本笔记只取代其中原有的重试时序。

Settings 外壳是恢复功能专用消费方,因此直接注入 Connection;普通功能代码仍使用 ctx.remote。它的私有 hooks compartment 绑定状态 observable 与重连命令。展开的侧边栏在 Settings 右侧渲染 ConnectionIndicator:disconnected 是浅黄色的连接异常操作;connecting 以黄色显示自动重连中,其中一至三个点每 500ms 前进一次,与 retry 时序无关;恢复后则以浅绿色显示连接成功并驻留 2 秒。鼠标悬浮或键盘聚焦任一黄色状态时只把文字改为立即重连;按压反馈采用轻微的警告色过渡,不使用原生 title tooltip。所有可见状态都为最宽的本地化文字预留空间,并使用固定的图标列和左对齐文字列,因此状态变化不会移动控件或改变其宽度。首次启动和未曾中断的健康连接都不渲染。

Alternatives considered

**固定每 2 秒重试且不进入终态。**不采用,因为长时间故障会持续产生连接流量。保留的指数策略先快速重试,再逐步降低频率,并在重试期间保留恢复操作。达到间隔上限后的重试由持续恢复决策规定。

**在视口顶部渲染全宽 ConnectionBanner。**不采用,因为状态应放在用户指定的恢复操作旁,全局覆盖层还会占用无关页面界面框架。该原语是内联 ConnectionIndicator;首次标签发布前不存在 ConnectionBanner 兼容导出。

**通过 ctx.remote.$connection 暴露生命周期控制。**不采用,因为 retry 状态与命令属于 Connection 服务,而不是 Remote 方法 namespace。直接使用 ctx.connection 仍是例外;本指示器本身负责控制重连,因此符合该例外。

**仅在用户点击时重试。**不采用,因为用户没有观察页面时仍必须自动恢复;按钮会重置退避并跳过当前等待。

Consequences

空闲浏览器连接的心跳流量会高于原默认值;长时间故障则按持续恢复决策保留间隔封顶的连接尝试。部署仍可覆盖 Host Ping 间隔。Gateway mux 不拥有第二个 retry timer,因此每条 retry #N warning 都对应一次由 Controller 请求的物理尝试。

手动重连会刻意中断共享物理 socket 的全部 logical Remote stream。它们既有的 generation supervisor 会通过新 baseline 或 cursor 恢复状态;单向通知仍不重放。

连接状态与浏览器网络输入都位于 React-free 传输层。Settings 组件只接收框架绑定的 selector hook 与普通回调,因此没有 UI store 复制传输状态;只有 2 秒成功提示和 500ms 点动画属于展示层本地状态。

Testing

Connection 与 Gateway 测试固定 2 秒心跳及 Pong deadline、指数 retry 间隔上限与日志、浏览器离线暂停和在线重置、手动重置序列、每次请求只替换一个 socket、状态去重、listener 隔离与 dispose。组件测试固定健康状态下不显示、悬浮与操作文案、独立点动画、点击行为与 2 秒成功状态。组装 Web 测试通过随附浏览器应用驱动浏览器 offline/online 转换、失败的 WebSocket 尝试、稳定的指示器几何、手动恢复与成功确认。