# 状态同步

> 独立于主流程之外的横切关注点：每当连接建立、事件到达、数据不一致或重新连接时，都需要更新频道、成员和服务器信息。
> 本流程不直接对应某个用户操作，而是被所有用户流程在特定步骤中触发。
> 所有 API 调用通过 `TSBridge`，事件通过 `EventBus` 接收。

---

## 一、触发时机总览

```
触发状态同步的时机
│
├─ ① 首次同步（连接建立后）
│     触发源：EventBus Connected 事件
│     动作：TSBridge.getChannelsJSON() + TSBridge.getClientsJSON() + TSBridge.getClientID()
│     目标：建立完整的频道基线和成员基线
│
├─ ② 增量同步（事件驱动）
│     触发源：EventBus ClientEnter / ClientMoved / ClientLeave 事件
│     动作：debounce 合并后 refreshClientList() 全量刷新
│     目标：维护实时成员位置和频道人数
│
├─ ③ 补偿同步（数据不一致时）
│     触发源：事件引用了未知的 clientID 或 channelID
│     动作：重新调用 TSBridge.getClientsJSON() 或 TSBridge.getChannelsJSON()
│     目标：用完整列表修复丢失的基线数据
│
├─ ④ 自身状态同步（频道切换后）
│     触发源：EventBus 收到自己的 ClientMoved 事件
│     动作：比对 clientID == selfID → 更新自身所在频道
│     目标：确认当前用户的实际频道位置
│
├─ ⑤ 消息归档同步（收到消息时）
│     触发源：EventBus TextMessage 事件
│     动作：按 targetMode + target 归档到正确的会话
│     目标：维护消息与目标频道的对应关系
│
├─ ⑥ 重连后全量同步（断开重连后）
│     触发源：重新收到 EventBus Connected 事件
│     动作：清空旧会话状态 → 重新执行首次同步
│     目标：确保重连后数据与服务器完全一致
│
└─ ⑦ 同步失败处理
      触发源：TSBridge.getChannelsJSON() / TSBridge.getClientsJSON() 请求失败
      动作：标记同步失败 → 不进入业务就绪 → 允许重试
      目标：防止在不完整数据上执行业务操作
```

---

## 二、同步状态树

```mermaid
flowchart TD
    SYNC[同步状态<br/>建立服务器数据基线]

    SYNC --> S0[未同步 Unsynced<br/>已连接但尚无完整数据]
    SYNC --> S1[同步中 Syncing<br/>调用 getChannelList 和 getClientList]
    SYNC --> S2[已同步 Synchronized<br/>列表基线可供 UI 使用]
    SYNC --> S3[同步失败 SyncFailed<br/>列表请求失败或上下文失效]

    S0 --> S1
    S1 --> S2
    S1 --> S3
    S3 --> S1
    S2 --> S1
```

---

## 三、成员状态树

```mermaid
flowchart TD
    MEMBER[成员状态<br/>维护在线用户及频道位置]

    MEMBER --> M0[空基线 Empty<br/>尚未执行 ListClients]
    MEMBER --> M1[已有基线 Loaded<br/>getClientList 已建立成员表]
    MEMBER --> M2[增量更新 Updating<br/>OnClientEnter/Leave/Moved 正在归并]
    MEMBER --> M3[不一致 Inconsistent<br/>事件引用未知成员或频道]

    M0 --> M1
    M1 --> M2
    M2 --> M1
    M2 --> M3
    M3 --> M1
```

---

## 四、成员实体状态树

```mermaid
flowchart TD
    ENTITY[成员实体状态树<br/>描述 ListClients 基线与实时事件如何协同]

    ENTITY --> BASE[基线分支<br/>由客户端请求建立完整成员集合]
    ENTITY --> DELTA[增量分支<br/>由服务端推送维护实时变化]
    ENTITY --> SELF[当前用户分支<br/>通过 ClientID 识别自己的事件]
    ENTITY --> REPAIR[修复分支<br/>处理事件缺失或引用不一致]

    BASE --> B1[TSBridge.getClientsJSON 请求<br/>获取服务器当前全部在线客户端]
    B1 --> B2[按 clientID 建表<br/>使用客户端 ID 去重保存]
    B2 --> B3[按 channelID 建索引<br/>派生每个频道的成员列表]

    DELTA --> D1[EventBus ClientEnter 事件<br/>debounce 合并后全量刷新]
    DELTA --> D2[EventBus ClientMoved 事件<br/>debounce 合并后全量刷新]
    DELTA --> D3[EventBus ClientLeave 事件<br/>debounce 合并后全量刷新]

    SELF --> S1[TSBridge.getClientID 本地调用<br/>读取服务器分配的自身 ID]
    S1 --> S2[事件 clientID 比对<br/>判断移动或离开事件是否属于自己]
    S2 --> S3[更新自身频道事实<br/>自己的移动决定实际所在频道]

    REPAIR --> R1[检测未知 clientID<br/>移动或离开事件找不到实体]
    R1 --> R2[重新调用 TSBridge.getClientsJSON<br/>用完整列表修复成员基线]
    REPAIR --> R3[检测未知 channelID<br/>成员引用本地不存在的频道]
    R3 --> R4[重新调用 TSBridge.getChannelsJSON<br/>用完整列表修复频道基线]
```

---

## 五、时序：首次同步

```mermaid
sequenceDiagram
    participant UI as 上层应用<br/>发起同步并更新状态
    participant VM as ViewModel<br/>收集 EventBus 事件
    participant BRIDGE as TSBridge<br/>调用 gomobile API
    participant SDK as Go SDK (gomobile)<br/>发送列表请求
    participant TS as TeamSpeak 服务器<br/>返回频道和成员数据
    participant STORE as 业务状态仓库<br/>保存基线数据

    Note over VM: EventBus Connected 事件触发首次同步

    par 请求频道基线
        VM->>BRIDGE: TSBridge.getChannelsJSON()
        BRIDGE->>SDK: gomobile 调用 GetChannelsJSON
        SDK->>TS: channellist
        TS-->>SDK: JSON 字符串
        SDK-->>BRIDGE: 频道 JSON
    and 请求成员基线
        VM->>BRIDGE: TSBridge.getClientsJSON()
        BRIDGE->>SDK: gomobile 调用 GetClientsJSON
        SDK->>TS: clientlist
        TS-->>SDK: JSON 字符串
        SDK-->>BRIDGE: 成员 JSON
    and 读取自身 ID
        VM->>BRIDGE: TSBridge.getClientID()
        BRIDGE-->>VM: selfClientID
    end

    alt 全部成功
        VM->>STORE: 原子提交频道、成员和自身 ID
        STORE-->>UI: 同步完成 → 状态进入 Synchronized
    else 任一失败
        VM->>STORE: 标记同步失败 → 状态进入 SyncFailed
        Note over UI: 允许重试，不进入业务就绪
    end
```

---

## 六、时序：增量同步与补偿同步

```mermaid
sequenceDiagram
    participant TS as TeamSpeak 服务器<br/>产生成员变化事件
    participant SDK as Go SDK (gomobile)<br/>解析 notify* 通知
    participant BRIDGE as TSBridge<br/>JNI 回调转 EventBus
    participant EB as EventBus<br/>事件合并与分发
    participant VM as ViewModel<br/>debounce 后刷新成员
    participant STORE as 成员实体仓库<br/>以 clientID 保存唯一成员
    participant UI as 频道树 UI<br/>派生人数和成员预览

    Note over TS,UI: ② 增量同步 — 正常事件流

    TS-->>SDK: notifycliententerview
    SDK->>BRIDGE: OnClientEnter(Client)
    BRIDGE->>EB: emit(ClientEnter)

    TS-->>SDK: notifyclientmoved
    SDK->>BRIDGE: OnClientMoved(id, channelID)
    BRIDGE->>EB: emit(ClientMoved)

    TS-->>SDK: notifyclientleftview
    SDK->>BRIDGE: OnClientLeave(id, reason)
    BRIDGE->>EB: emit(ClientLeave)

    Note over EB: debounce 300ms 合并

    EB->>VM: collect → refreshClientList()
    VM->>STORE: TSBridge.getClientsJSON() → 全量刷新

    alt 刷新成功
        STORE-->>UI: 基线已更新
    else 数据不一致
        Note over VM: ③ 补偿同步 — 修复丢失的基线
        VM->>STORE: TSBridge.getChannelsJSON() → 修复频道基线
        STORE-->>UI: 基线已修复
    end
```

---

## 七、时序：重连后全量同步

```mermaid
sequenceDiagram
    participant UI as 上层应用<br/>处理重连流程
    participant VM as ViewModel<br/>管理清理和重建
    participant BRIDGE as TSBridge<br/>管理连接生命周期
    participant SDK as Go SDK (gomobile)<br/>重新建立连接
    participant STORE as 会话数据仓库<br/>保存会话临时事实

    Note over UI: 网络恢复或手动重连

    UI->>VM: 检测到需要重连
    VM->>VM: 状态改为 disconnecting
    VM->>STORE: 清理旧会话数据
    STORE-->>VM: 清理完成

    Note over VM: 重新执行连接流程

    VM->>BRIDGE: TSBridge.connect(...)
    BRIDGE->>SDK: gomobile Connect
    SDK-->>BRIDGE: OnConnected 回调
    BRIDGE->>BRIDGE: EventBus.emit(Connected)
    BRIDGE->>VM: EventBus 分发 Connected 事件

    Note over VM: 重新执行首次同步

    VM->>BRIDGE: getChannelsJSON + getClientsJSON + getClientID
    BRIDGE->>SDK: gomobile 调用
    SDK-->>BRIDGE: 完整基线数据
    VM->>STORE: 原子提交新基线
    STORE-->>UI: 同步完成 → 恢复业务就绪
```

---

## 八、同步与主流程的关系

```mermaid
flowchart TD
    subgraph 主流程触发点
        A[01 连接服务器<br/>EventBus Connected]
        B[02 浏览频道<br/>EventBus ClientEnter/Moved/Leave]
        C[03 切换频道<br/>EventBus ClientMoved]
        D[04 文本消息<br/>EventBus TextMessage]
        E[07 断开连接<br/>重新连接]
    end

    subgraph 状态同步流程
        F[① 首次同步]
        G[② 增量同步]
        H[③ 补偿同步]
        I[④ 自身状态同步]
        J[⑤ 消息归档同步]
        K[⑥ 重连后全量同步]
        L[⑦ 同步失败处理]
    end

    A --> F
    B --> G
    B --> H
    C --> I
    D --> J
    E --> K
    F --> L
    K --> F
    H --> F
```

---

## 九、事件依赖

### 同步相关前置依赖矩阵

| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| 首次同步 | EventBus `Connected` 事件 | 同时读取频道、成员和自身 ID | 任一核心请求失败则不标记业务就绪 |
| `getClientsJSON` 增量归并 | 已有频道基线更易校验 | 与 `getChannelsJSON` 同一同步批次 | 未知频道触发频道补偿读取 |
| `ClientEnter` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
| `ClientMoved` 事件 | 已注册 EventBus 事件收集 | 已有成员基线和自身 ID | debounce 后全量刷新 |
| `ClientLeave` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
| 提交自己新频道 | 自己的 `ClientMoved` 事件 | 匹配目标频道和请求上下文 | 命令响应不能直接提交频道 |
| `TextMessage` 归档 | `targetMode`、`target`、发送者信息 | 已有频道或私聊实体 | 不得使用当前页面猜测目标 |

### 权威性划分

| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 初始频道列表 | `TSBridge.getChannelsJSON()` 响应 | SDK 未列出频道创建、更新、删除事件 |
| 初始在线成员 | `TSBridge.getClientsJSON()` 响应 | 建立完整在线成员基线 |
| 自身客户端 ID | `TSBridge.getClientID()` | SDK 本地缓存服务器分配的 ID |
| 成员实时变化 | EventBus `ClientEnter/Leave/Moved` 事件 | 服务端主动推送实时事实，debounce 后全量刷新 |
| 文本消息 | EventBus `TextMessage` 事件 | 来源是 `notifytextmessage` |

### 缺失事件能力带来的依赖限制

根据 SDK 文档，当前公开事件不包含频道创建、频道更新和频道删除。因此：

1. `TsClient.getChannelList()` 是频道目录的主要权威来源。
2. 不能假设频道列表会依靠 `onClient*` 事件永久保持最新。
3. 发现成员引用未知频道时，应重新调用 `TsClient.getChannelList()`。
4. 如果产品需要实时频道管理，应扩展 SDK 对相应 `notify*` 通知的支持。

### 统一实现原则（同步相关）

- **列表命令建立基线，服务端事件维护增量。** 两者缺一不可。
- **成员表按 ClientID 幂等更新。** debounce 合并后 refreshClientList 全量刷新，天然幂等。
- **当前用户通过 ClientID 识别。** 不依赖昵称或 UI 当前页面判断自己。
- **未知实体引用触发补偿同步。** 不静默忽略移动或离开事件。
- **消息按 TargetMode 和 Target 归档。** 不使用当前页面频道代替真实目标。
- **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
- **事件通过 EventBus 分发，不在 JNI 回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`，ViewModel 通过 `collect()` 响应。
