主题
事件与回调详细说明
适用版本:0.1 Commercial Beta 覆盖:请求完成、账号事件流、自动续凭 Provider
XHIM 把一次请求结果和账号长期事件明确分开。iOS 与 macOS 新项目可分别使用 onSuccess / onFailure,已经采用 Swift Concurrency 的项目使用同名异步 重载;两者再通过一个账号级监听接收长期变化:
- 请求完成:某次调用成功或失败;
- 投影变化:本地消息、会话、好友、群组等可查询状态已经改变;
- 瞬时状态:在线状态、正在输入等带有效期的实时提示;
- 续凭请求:SDK 需要业务账号层提供新凭证。
1. 回调入口
1.1 一次性请求完成
| 平台 | 成功 | 失败 | 取消 |
|---|---|---|---|
| iOS | onSuccess(Value) | onFailure(XHIMCallbackError) | XHIMRequest.cancel() |
| macOS | onSuccess(Value) | onFailure(XHIMCallbackError) | XHIMRequest.cancel() |
| Android | suspend 返回值 | 抛出 XHIMException | 取消 Coroutine |
| Windows | Task<T> 完成 | 抛出 XHIMException | CancellationToken |
| HarmonyOS | Promise<T> resolve | reject XHIMError | cancelRequest(requestId) |
Completion 在 XHIM 私有 callback 执行器产生。Facade 会先复制 native 借用 内存;iOS/macOS 回调式 Facade 再按顺序切换到 MainActor。不要在回调中同步阻塞 等待另一个 XHIM 请求。
成功的精确定义由 API 决定。例如:
sendText成功:Outbox 已持久接受消息,不代表已经送达;acceptMediaUpload成功:媒体任务已持久接受,不代表上传完成;markConversationRead成功:本地已读水位和待同步事实已更新;- 查询成功:返回该调用时刻的本地投影快照。
失败时只依赖稳定字段:
| 字段 | 用法 |
|---|---|
domain | 错误子系统 |
stableCode | 业务分支和统计键 |
retryable | SDK 是否允许同幂等键重试 |
retryAfterMilliseconds | 建议最短重试等待 |
userAction | 重试登录、检查网络或联系管理员 |
operationID / traceID | 客服与服务端排障 |
message | 仅展示/日志,不做字符串匹配 |
1.2 账号事件流
swift
let eventToken = client.addEventListener { event in
viewModel.consume(event)
}
// 账号容器销毁时:
eventToken.cancel()kotlin
val eventJob = accountScope.launch {
client.events.collect { event ->
withContext(Dispatchers.Main.immediate) {
viewModel.consume(event)
}
}
}csharp
await foreach (var evt in client.Events(accountCancellationToken))
{
await dispatcher.InvokeAsync(() => viewModel.Consume(evt));
}ts
const off = client.onEvent((event: XHIMEvent): void => {
this.viewModel.consume(event)
})
// 账号容器销毁时:
off()必须先安装订阅者,再查询首屏。事件队列有界,默认只保留最近值;事件本身也 不是可重放业务日志。因此正确用法永远是“收到事件后重查 SDK 投影”,不能靠 累加事件 payload 维护第二份数据库。
1.3 自动续凭 Provider
swift
let request = XHIMClient.connect(
server: "https://im.example.com",
userID: account.userID,
appID: "your-app-id",
authentication: .business(accountAPI.fetchXHIMAccessToken),
onConnecting: {
viewModel.connectionState = .connecting
},
onConnectSuccess: { client in
accountContainer.client = client
},
onConnectFailure: { error in
viewModel.show(error.message)
}
)触发条件:首次连接需要凭证,或服务端/Adapter 报告凭证失效。SDK 会合并同一 账号生命周期内的并发刷新,只让一个 Provider 执行;成功后自动 updateCredential → reconnect → sync → ready。Provider 不在页面层实现, 不得返回管理员密钥,也不要记录 Token 文本。
2. 共同投影载荷
messageUpserted、messageStateChanged、conversationChanged、 socialChanged、syncApplied、projectionInvalidated 携带 XHIMProjectionChange:
| 字段 | 含义与处理 |
|---|---|
schemaVersion | 当前已知版本为 1;未知版本执行完整重查 |
kind / nativeKind | 已知投影类型和未来原始数值 |
origin / nativeOrigin | local、remoteSync、outbox 或未来值 |
socialScope | 好友申请、好友、群、群成员、黑名单、入群申请 |
messageState | 消息状态变化时的状态提示,最终值仍以查询为准 |
accountEpoch | 登录账号代次;旧账号事件不得更新新账号 UI |
connectionGeneration | 重连代次 |
projectionRevision | Client 级递增失效序号;跳号后完整重查 |
entityRevision | 相关实体 revision |
sequence | 相关消息或同步序号 |
affectedCount | 批量更新的大致影响数 |
conversationID | 可用时缩小到单个会话 |
clientMessageID | 可用时缩小到单条本地消息 |
serverMessageID | 可用时定位服务端消息 |
scopeID | 社交域对象或用户范围 |
requiresFullRequery | 为 true 时禁止解释未知载荷,直接重查 |
所有投影事件的可靠性等级均为 失效提示:同一 Client 内按已接纳顺序投递, 允许有界队列覆盖旧提示、合并或因账号 epoch 切换丢弃旧事件。业务事实不会因此 丢失,因为事实已在 SDK 数据库;页面需要重查。
3. stateChanged
原型与载荷
swift
case stateChanged(XHIMClientState)状态值:created、started、authenticating、connecting、 synchronizing、ready、credentialRequired、loggingOut、closed、 fatal。
触发条件
Client 生命周期、登录、断线重连、同步、退出或不可恢复错误引起状态迁移时触发。 同一状态可能因新的 connection generation 再次出现,不要把它当作只触发一次。
收到后的动作
- 只有
ready允许业务写操作; connecting/synchronizing显示非阻塞连接提示;credentialRequired由自动续凭 Provider 处理,页面不索要 Token;fatal读取稳定错误和诊断信息,停止自动业务重试。
关联 API:lifecycle.connect、lifecycle.start、lifecycle.login、 lifecycle.update_credential、lifecycle.logout、lifecycle.shutdown。
4. messageQueued
原型与载荷
swift
case messageQueued(Data)Data 是兼容性回执载荷,不是完整 XHIMMessage,页面不要自行解析成长期模型。
触发条件
sendText、sendMessage 或媒体任务的依赖消息被本地 Outbox 持久接受时触发。 触发不表示已上传到服务端或已送达对端。
收到后的动作
使用调用方保存的 clientMessageID 调用 message(clientMessageID:),或重查 当前会话 messages。渲染查询返回的 XHIMMessage.state。
可靠性:失效提示;可被后续 messageUpserted / messageStateChanged 覆盖。
关联 API:message.send_text、message.send_custom、media.send_image、 media.send_video、media.send_audio、media.send_file。
5. messageRetryQueued
原型与载荷
swift
case messageRetryQueued(Data)触发条件
retryMessage(clientMessageID:) 成功把可重试失败消息重新放入 Outbox 时触发。 它不表示本次重试已经成功发送。
收到后的动作
按 clientMessageID 重查消息,刷新“发送中/失败”状态和重试按钮。
可靠性:失效提示。
关联 API:message.retry、message.get。
6. messageCancelled
原型与载荷
swift
case messageCancelled(Data)触发条件
cancelMessage(clientMessageID:) 成功取消仍可取消的 Outbox 消息时触发。已经成为 服务端事实的消息不能靠该 API 撤销,应使用 recall。
收到后的动作
重查目标消息;根据查询结果隐藏进度、显示取消状态或删除临时 UI。
可靠性:失效提示。
关联 API:message.cancel、message.recall、message.get。
7. conversationReadChanged
原型与载荷
swift
case conversationReadChanged(String) // conversationID触发条件
单会话已读水位、全部会话已读或远端同步的已读状态改变时触发。
收到后的动作
同时重查:
- 该会话摘要;
totalUnreadCount();- 页面展示对端已读时,重查
conversationPeerReads。
可靠性:失效提示;批量已读可能只需要一次会话列表全量刷新。
关联 API:conversation.mark_read、conversation.mark_all_read、 conversation.total_unread、conversation.peer_reads。
8. messageMutated
原型与载荷
swift
case messageMutated(String) // clientMessageID触发条件
消息编辑、撤回或仅对自己删除完成并更新本地可见投影时触发。
收到后的动作
有 clientMessageID 时调用 message(clientMessageID:);消息已不可见时重查 时间线。不要只修改当前 Cell 的文本,因为 revision、预览和会话摘要也可能变化。
可靠性:失效提示。
关联 API:message.edit_text、message.recall、 message.delete_for_self。
9. mediaTaskUpdated
原型与载荷
swift
case mediaTaskUpdated(XHIMMediaTaskUpdate)| 字段 | 含义 |
|---|---|
taskID | 持久媒体任务 ID |
state / nativeState | 已知任务状态和未来原始数值 |
schemaVersion | 提示载荷版本 |
触发条件
上传/下载任务排队、获得授权、传输、校验、退避重试、完成、失败或取消时触发。
收到后的动作
总是调用 mediaTask(taskID:) 获取完整快照。终态为 completed 后才能打开缓存或 确认依赖消息已入队;失败时依据 failureRetryable、lastFailureCode、 nextAttemptAtMilliseconds 展示动作。
可靠性:状态提示会合并;中间进度不保证逐字节到达,终态持久保存在数据库。
关联 API:全部 media.* 任务 API。
10. messageUpserted
原型与载荷
swift
case messageUpserted(XHIMProjectionChange)触发条件
本地发送、远端同步、历史同步或消息 mutation 使消息投影新增/替换时触发。
收到后的动作
conversationID非空:重查该会话messages;clientMessageID非空且页面只关心单条:可调用message;requiresFullRequery或 revision 跳号:重查当前页面的完整时间线。
可靠性:投影失效提示。
关联 API:message.get、message.list、message.history。
11. messageStateChanged
原型与载荷
swift
case messageStateChanged(XHIMProjectionChange)触发条件
Outbox 或远端回执使消息从 queued/sending 进入 sent、delivered、read、failed、 cancelled 等新状态时触发。
收到后的动作
按 clientMessageID 重查消息;如果 ID 缺失或 revision 不连续,重查会话 时间线。change.messageState 只用于快速判断刷新优先级,不作为最终状态源。
可靠性:投影失效提示;快速状态迁移可能只观察到较新的提示。
关联 API:message.get、message.list、message.retry、 message.cancel。
12. conversationChanged
原型与载荷
swift
case conversationChanged(XHIMProjectionChange)触发条件
最后一条消息、未读数、已读水位、置顶、免打扰、草稿、清空/隐藏视图等会话摘要 字段改变时触发。
收到后的动作
重查 conversations。如果页面只展示指定会话,也要用查询结果替换整个会话 ViewModel,避免列表排序和总未读不同步。
可靠性:投影失效提示。
关联 API:全部 conversation.* API,以及发送和消息 mutation API。
13. socialChanged
原型与载荷
swift
case socialChanged(XHIMProjectionChange)触发条件
好友申请、好友关系、好友备注、黑名单、群、群成员、入群申请或群治理投影改变 时触发。
收到后的动作
按 socialScope 重查:
socialScope | 查询 |
|---|---|
friendRequests | friendRequests |
friendships | friendships |
groups | groups |
groupMembers | groupMembers(conversationID:) |
blocks | blocks |
groupJoinRequests | groupJoinRequests |
none / unknown | 重查当前页面依赖的全部社交投影 |
可靠性:投影失效提示;同一治理操作可能影响多个 scope。
关联 API:全部 relationship.* 和 group.* API。
14. syncApplied
原型与载荷
swift
case syncApplied(XHIMProjectionChange)触发条件
一批服务端增量同步事务成功写入本地投影后触发。affectedCount 是批量提示,不是 完整实体列表。
收到后的动作
刷新当前可见页面依赖的查询;首轮同步结束仍以 stateChanged(.ready) 作为允许 写入的门槛。不要尝试从 sequence 自行向服务器补拉数据。
可靠性:批次级失效提示,可合并。
关联 API:lifecycle.connect、lifecycle.notify_network_available 和全部 本地查询 API。
15. presenceChanged
原型与载荷
swift
case presenceChanged(XHIMPresenceUpdate)| 字段 | 含义 |
|---|---|
eventID | 实时事件 ID,用于短期去重 |
userID | 状态所属用户 |
status / nativeStatus | online、away、offline 或未来值 |
sequence | 该实时流顺序提示 |
expiresAtMilliseconds | 到期时间 |
触发条件
订阅范围内用户发布在线状态,或实时服务下发状态变化时触发。
收到后的动作
按 userID 更新内存 UI 状态;到达 expiresAtMilliseconds 必须本地过期为未知/ 离线。它不是持久用户资料,不写入长期业务缓存。
可靠性:瞬时、允许丢失和覆盖;重连后不得继续展示已经过期的值。
关联 API:user.publish_presence。
16. typingChanged
原型与载荷
swift
case typingChanged(XHIMTypingUpdate)| 字段 | 含义 |
|---|---|
eventID | 实时事件 ID |
conversationID | 会话 |
userID | 输入中的用户 |
isTyping | 开始或停止输入 |
sequence | 短期顺序提示 |
expiresAtMilliseconds | 强制过期时间 |
触发条件
会话成员调用 publishTyping 或实时服务下发输入状态时触发。
收到后的动作
只更新当前会话的瞬时提示;收到 false 或到期时清除。调用方应节流“开始输入” 发布,并在停止、离开页面时发布 false,但接收方仍必须依赖 TTL 兜底。
可靠性:瞬时、允许丢失和覆盖,不用于审计。
关联 API:user.publish_typing。
17. projectionInvalidated
原型与载荷
swift
case projectionInvalidated(XHIMProjectionChange)触发条件
Facade 收到未来 schemaVersion、未知 projection kind,或无法安全映射的新载荷 时触发。这是向前兼容保护,不是普通业务错误。
收到后的动作
不要 switch 未知原始值推测业务含义。对当前页面依赖的消息、会话或社交投影 执行完整重查;同时记录 nativeKind、schemaVersion 和 SDK 版本供兼容性 排查,不记录消息正文或凭证。
可靠性:保护性失效提示。
关联 API:所有本地查询 API、lifecycle.compatibility。
18. 页面级处理模板
swift
@MainActor
final class ConversationListViewModel: ObservableObject {
@Published private(set) var conversations: [XHIMConversation] = []
@Published private(set) var unreadCount: Int64 = 0
private let client: XHIMClient
private var eventTask: Task<Void, Never>?
init(client: XHIMClient) {
self.client = client
}
func start() {
eventTask = Task { [weak self, client] in
for await event in client.events {
guard !Task.isCancelled else { return }
switch event {
case .conversationChanged,
.conversationReadChanged,
.syncApplied,
.projectionInvalidated:
await self?.reload()
default:
break
}
}
}
Task { await reload() }
}
private func reload() async {
do {
async let page = client.conversations(limit: 50)
async let unread = client.totalUnreadCount()
let (loadedPage, loadedUnread) = try await (page, unread)
conversations = loadedPage.conversations
unreadCount = loadedUnread
} catch is CancellationError {
// 页面结束,不展示错误。
} catch {
// 映射 XHIM 稳定错误字段后交给页面状态。
}
}
deinit {
eventTask?.cancel()
}
}这个模板刻意不在事件 switch 中拼接/删除本地数组;所有长期状态都重新查询 SDK,因而能够承受事件合并、重连、跨设备同步和未来 schema。