主题
事件与回调详细说明
适用版本: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 请求。
用户公开业务扩展的 C ABI 回调为 xhim_v1_user_profile_envelope_completion_callback。成功时 profile 是冻结的旧 ABI 资料前缀,extension.application_extension_json 是服务端 规范化后的权威公开 JSON。两者和所有嵌套 byte view 都只在回调返回前 有效;异步 UI/缓存必须先深拷贝。旧 xhim_v1_user_profile_completion_callback 继续只交付基础资料,不得通过 隐式全局表或 thread_local 补扩展。批量回调需在回调内调用 xhim_v1_user_profile_page_get_application_extensions,扩展数组与 profiles 等长且同下标。
申请/方向关系扩展遵循同一规则:
- 带扩展发送好友申请返回
XHIMFriendRequestEnvelope; - 扩展感知的好友申请页在 callback 内通过
xhim_v1_social_page_get_request_extensions取得与 item 等长的平行数组; - 扩展感知处理好友申请返回原申请扩展;
- 黑名单和入群申请的扩展感知回调使用新 snapshot 尾部, 基础回调不读该尾部。
上述结果的嵌套 view 都是 callback 期借用内存。Apple/Android/ Windows/HarmonyOS 在完成语言层 callback/continuation 前已深拷贝; 不应在业务层保存原生指针或借用 Data/byte view。为防止 隐私泄漏,默认错误、description 和诊断只能记录“是否变更/字节数/ 是否清除”,不记录 JSON 正文。
基础入口与扩展感知入口的 completion 不同:基础入口在旧 Core 上 继续成功,扩展可用性是 unknown;显式 *WithExtension(s) 缺少 dedicated weak symbol/accessor 时在提交变更前失败为 UNSUPPORTED。扩展感知 completion 成功时的空值才是 authoritative clear,不得用空值代替 unknown。 对应的 Server capability 未声明时也必须在 mutation 前返回 UNSUPPORTED;只检查 weak symbol 不足以防止“新 Core + 旧 Server”静默忽略 新 tail。
成功的精确定义由 API 决定。例如:
sendText成功:Outbox 已持久接受消息,不代表已经送达;acceptMediaUpload成功:媒体任务已持久接受,不代表上传完成;markConversationRead成功:本地已读水位和待同步事实已更新;- 查询成功:返回该调用时刻的本地投影快照。
- 社交投影完整性检查成功:返回 proof-backed snapshot;其中
complete=false是可预期结果,必须按reason处理,不代表 completion 失败, 也不会触发任何自动修复。 - 群成员完整投影刷新成功:
xhim_v1_client_refresh_group_member_projection也复用xhim_v1_social_projection_integrity_completion_callback,但只有远端无筛选全量分页、 revision/游标/成员数/唯一群主校验和 SQLite 原子替换全部成功才返回complete=true。revision 漂移返回INVALID_STATE、retryable=1和group_member_projection_revision_stale;不会返回伪造的修复 snapshot。 - 扩展感知申请列表成功:平行扩展数组的数量、下标和
requestID已校验;空 JSON 是权威清除,不是“旧 Core 不知道”。
失败时只依赖稳定字段:
| 字段 | 用法 |
|---|---|
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、messageLocallyDeleted、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。
10.1 messageLocallyDeleted
原型与载荷
swift
case messageLocallyDeleted(XHIMProjectionChange)触发条件
message.delete_local 或 message.delete_all_local 在当前设备新增了一条或多条 本地删除 tombstone。这不是服务端删除或跨设备同步事件。
收到后的动作
按 conversationID 重查当前时间线;当会话 ID 为空时重查当前账号正在 显示的时间线与会话页。不要按旧 IndexPath 直接删 UI 数组。
可靠性:本地 durable projection 失效提示;实际可见数据以重查结果为准。
关联 API:message.delete_local、message.delete_all_local。
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)触发条件
最后一条消息、未读数、已读水位、置顶、免打扰、账号私有会话业务扩展、 草稿、清空/隐藏视图等会话摘要字段改变时触发。事件本身不携带 扩展 JSON。
收到后的动作
重查 conversations。如果页面只展示指定会话,也要用查询结果替换整个会话 ViewModel,避免列表排序和总未读不同步。 需要扩展的页面必须使用能证明扩展 accessor 可用的平台查询结果; 旧 Core 上基础会话列表仍可用,但不应把无扩展投影解释为权威空值。
可靠性:投影失效提示。
关联 API:全部 conversation.* API,以及发送和消息 mutation API。
13. socialChanged
原型与载荷
swift
case socialChanged(XHIMProjectionChange)触发条件
好友申请、好友关系、好友备注、黑名单、群、群成员、入群申请或群治理投影改变 时触发。
收到后的动作
按 socialScope 重查:
socialScope | 查询 |
|---|---|
friendRequests | 基础页用 friendRequests;页面需要业务扩展时必须用 friendRequestsWithExtensions |
friendships | friendships |
groups | groups |
groupMembers | groupMembers(conversationID:) |
blocks | 基础页用 blocks;需要当前账号私有扩展时用 blocksWithExtensions |
groupJoinRequests | 基础页用 groupJoinRequests;审批页需要 metadata 时用 groupJoinRequestsWithExtensions |
none / unknown | 重查当前页面依赖的全部社交投影 |
可靠性:投影失效提示;同一治理操作可能影响多个 scope。
扩展不会改变事件类型,也不把 JSON 放进事件 payload。收到 socialChanged 后必须按当前页面的数据合同重查:基础页使用基础 API,扩展页使用 *WithExtensions。扩展页在旧 Core 上得到 UNSUPPORTED 时要展示明确的版本/能力状态,不能降级为基础页后 把 unknown 渲染成“已清空”。
关联 API:全部 relationship.*、group.* 和 projection.* 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 或未来值 |
activePlatforms | 当前未过期 Presence 租约所属端的去重列表 |
sequence | 该实时流顺序提示 |
expiresAtMilliseconds | 到期时间 |
触发条件
订阅范围内用户发布在线状态,或实时服务下发状态变化时触发。
收到后的动作
按 userID 更新内存 UI 状态;到达 expiresAtMilliseconds 必须本地过期为未知/ 离线。它不是持久用户资料,不写入长期业务缓存。
可靠性:瞬时、允许丢失和覆盖;重连后不得继续展示已经过期的值。
关联 API:user.publish_presence、user.query_presence、 user.subscribe_presence、user.unsubscribe_presence、 user.list_presence_subscriptions。
16. typingChanged
原型与载荷
swift
case typingChanged(XHIMTypingUpdate)| 字段 | 含义 |
|---|---|
eventID | 实时事件 ID |
conversationID | 会话 |
userID | 输入中的用户 |
isTyping | 开始或停止输入 |
activePlatforms | 当前未过期 Typing 租约所属端的去重列表 |
sequence | 短期顺序提示 |
expiresAtMilliseconds | 强制过期时间 |
触发条件
会话成员调用 publishTyping 或实时服务下发输入状态时触发。
收到后的动作
只更新当前会话的瞬时提示;收到 false 或到期时清除。调用方应节流“开始输入” 发布,并在停止、离开页面时发布 false,但接收方仍必须依赖 TTL 兜底。
可靠性:瞬时、允许丢失和覆盖,不用于审计。
关联 API:user.publish_typing、user.query_typing。
17. customSignalReceived
原型与载荷
swift
case customSignalReceived(XHIMCustomSignal)| 字段 | 含义 |
|---|---|
eventID | 接收事件时的服务端事件 ID;发布完成回执中为空 |
conversationID / userID | 会话与发送者 |
clientSignalID | 发送方生成的信令 ID |
contentType / contentVersion | 应用自定义合同与版本 |
payload | 1...65536 字节的不透明载荷 |
sequence | 实时流顺序提示 |
expiresAtMilliseconds | 必须清理 UI 状态的最晚时间 |
只有当前在线会话成员会收到该回调。未知 contentType、不支持的 version 或已过期载荷必须直接忽略;不得回退为聊天消息或计入未读。 该事件允许丢失、覆盖和乱序,不用于订单、OA 审批、支付或审计。
关联 API:user.publish_custom_signal。
17.1 accountStateChanged
原型与载荷
swift
case accountStateChanged(XHIMAccountStateChange)| 字段 | 含义 |
|---|---|
eventID | 服务端账号事件 ID;可用于当次诊断,不作持久幂等键 |
state / nativeState | active、suspended、unregistered 或前向兼容 unknown 及原始值 |
kickedReason / nativeKickedReason | none、account suspended、account unregistered 及原始值 |
revision | 正整数为权威版本;0 是旧 frame 缺 tag 19,只能保守去重 |
accountEpoch | 当前登录账号代次;不匹配必须丢弃 |
connectionGeneration | 实时连接代次;用于屏蔽迟到回调 |
Android 事件是 XHIMEvent.AccountStateChanged,Windows 是 XHIMEvent.AccountStateChanged,HarmonyOS 是 XHIMAccountStateChangedEvent。Flutter 暴露 XHIMAccountStateChanged,Electron 通过 onAccountStateChanged返回 XHIMElectronAccountStateChange,Web 通过 on('accountStateChanged', ...) 返回 XHIMWebAccountStateChanged。
触发条件
参考实时流收到 ACCOUNT_STATE_CHANGED(17)。只有固定原因 account_suspended / account_unregistered 映射成已知剔除原因; 普通设备会话撤销仍走原 SESSION_REVOKED 路径,不伪造账号治理 事件。旧 Core 不认识 kind 17 时不产生该强类型回调。
收到后的动作
- 取消该账号下的普通在途请求,停止自动续凭和无限重试;
- 以账号 epoch 为边界销毁页面、数据仓库和事件订阅;
- suspended/unregistered 都回到业务登录页;恢复停用账号后也必须 重新签发 Token;
- 只用固定 stable code 选择本地可读文案。不查找、不显示管理员 原因,不将它记入分析或崩溃日志。
Facade 按 account epoch 屏蔽退出/关闭后迟到事件,按正整数 revision 屏蔽重复/降序事件。revision == 0 只屏蔽完全相同的状态+原因, 不将其保存为权威最新版本。
18. projectionInvalidated
原型与载荷
swift
case projectionInvalidated(XHIMProjectionChange)触发条件
Facade 收到未来 schemaVersion、未知 projection kind,或无法安全映射的新载荷 时触发。这是向前兼容保护,不是普通业务错误。
收到后的动作
不要 switch 未知原始值推测业务含义。对当前页面依赖的消息、会话或社交投影 执行完整重查;同时记录 nativeKind、schemaVersion 和 SDK 版本供兼容性 排查,不记录消息正文或凭证。
可靠性:保护性失效提示。
关联 API:所有本地查询 API、lifecycle.compatibility。
18. reliableBusinessNotificationReceived
公开面 ID:event.reliable_business_notification。
载荷
notificationEventID、serviceUserID、operationID、occurredAtMilliseconds 和 owned XHIMBusinessNotification。dataJSON == nil 表示服务端未提供 data; {} 表示权威空对象。正文、body、data 都在 native callback 返回前完成深拷贝。
触发条件与可靠性
服务端 notifications:notify 写入账号 Sync 流,本地投影事务和游标提交成功后触发。 它不创建消息/会话、不改变未读、不发 Push、不触发消息 webhook。相同 event ID 的 Sync 重放不重复触发;旧 Core 或未来/畸形 envelope 只推进可跳过事件,不伪造类型。
平台入口
- Swift:
case .reliableBusinessNotificationReceived(let value) - Kotlin:
XHIMEvent.ReliableBusinessNotificationReceived - C#:
XHIMEvent.ReliableBusinessNotificationReceived - HarmonyOS:
XHIMReliableBusinessNotificationReceivedEvent - Web:
client.on('reliableBusinessNotificationReceived', listener) - Electron:
client.onReliableBusinessNotificationReceived(listener);renderer IPC 只获得不含 title/body/data 的 allowlist 元数据 - Flutter:
XHIMReliableBusinessNotificationReceived
模型的 description / ToString() 默认脱敏;业务代码不得把正文或 data 写入日志。
19. 页面级处理模板
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。