Skip to content

事件与回调详细说明

适用版本:0.1 Commercial Beta 覆盖:请求完成、账号事件流、自动续凭 Provider

XHIM 把一次请求结果和账号长期事件明确分开。iOS 与 macOS 新项目可分别使用 onSuccess / onFailure,已经采用 Swift Concurrency 的项目使用同名异步 重载;两者再通过一个账号级监听接收长期变化:

  • 请求完成:某次调用成功或失败;
  • 投影变化:本地消息、会话、好友、群组等可查询状态已经改变;
  • 瞬时状态:在线状态、正在输入等带有效期的实时提示;
  • 续凭请求:SDK 需要业务账号层提供新凭证。

1. 回调入口

1.1 一次性请求完成

平台成功失败取消
iOSonSuccess(Value)onFailure(XHIMCallbackError)XHIMRequest.cancel()
macOSonSuccess(Value)onFailure(XHIMCallbackError)XHIMRequest.cancel()
Androidsuspend 返回值抛出 XHIMException取消 Coroutine
WindowsTask<T> 完成抛出 XHIMExceptionCancellationToken
HarmonyOSPromise<T> resolvereject XHIMErrorcancelRequest(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_STATEretryable=1group_member_projection_revision_stale;不会返回伪造的修复 snapshot。
  • 扩展感知申请列表成功:平行扩展数组的数量、下标和 requestID 已校验;空 JSON 是权威清除,不是“旧 Core 不知道”。

失败时只依赖稳定字段:

字段用法
domain错误子系统
stableCode业务分支和统计键
retryableSDK 是否允许同幂等键重试
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. 共同投影载荷

messageUpsertedmessageLocallyDeletedmessageStateChangedconversationChangedsocialChangedsyncAppliedprojectionInvalidated 携带 XHIMProjectionChange

字段含义与处理
schemaVersion当前已知版本为 1;未知版本执行完整重查
kind / nativeKind已知投影类型和未来原始数值
origin / nativeOriginlocalremoteSyncoutbox 或未来值
socialScope好友申请、好友、群、群成员、黑名单、入群申请
messageState消息状态变化时的状态提示,最终值仍以查询为准
accountEpoch登录账号代次;旧账号事件不得更新新账号 UI
connectionGeneration重连代次
projectionRevisionClient 级递增失效序号;跳号后完整重查
entityRevision相关实体 revision
sequence相关消息或同步序号
affectedCount批量更新的大致影响数
conversationID可用时缩小到单个会话
clientMessageID可用时缩小到单条本地消息
serverMessageID可用时定位服务端消息
scopeID社交域对象或用户范围
requiresFullRequerytrue 时禁止解释未知载荷,直接重查

所有投影事件的可靠性等级均为 失效提示:同一 Client 内按已接纳顺序投递, 允许有界队列覆盖旧提示、合并或因账号 epoch 切换丢弃旧事件。业务事实不会因此 丢失,因为事实已在 SDK 数据库;页面需要重查。

3. stateChanged

原型与载荷

swift
case stateChanged(XHIMClientState)

状态值:createdstartedauthenticatingconnectingsynchronizingreadycredentialRequiredloggingOutclosedfatal

触发条件

Client 生命周期、登录、断线重连、同步、退出或不可恢复错误引起状态迁移时触发。 同一状态可能因新的 connection generation 再次出现,不要把它当作只触发一次。

收到后的动作

  • 只有 ready 允许业务写操作;
  • connecting / synchronizing 显示非阻塞连接提示;
  • credentialRequired 由自动续凭 Provider 处理,页面不索要 Token;
  • fatal 读取稳定错误和诊断信息,停止自动业务重试。

关联 API:lifecycle.connectlifecycle.startlifecycle.loginlifecycle.update_credentiallifecycle.logoutlifecycle.shutdown

4. messageQueued

原型与载荷

swift
case messageQueued(Data)

Data 是兼容性回执载荷,不是完整 XHIMMessage,页面不要自行解析成长期模型。

触发条件

sendTextsendMessage 或媒体任务的依赖消息被本地 Outbox 持久接受时触发。 触发不表示已上传到服务端或已送达对端。

收到后的动作

使用调用方保存的 clientMessageID 调用 message(clientMessageID:),或重查 当前会话 messages。渲染查询返回的 XHIMMessage.state

可靠性:失效提示;可被后续 messageUpserted / messageStateChanged 覆盖。

关联 API:message.send_textmessage.send_custommedia.send_imagemedia.send_videomedia.send_audiomedia.send_file

5. messageRetryQueued

原型与载荷

swift
case messageRetryQueued(Data)

触发条件

retryMessage(clientMessageID:) 成功把可重试失败消息重新放入 Outbox 时触发。 它不表示本次重试已经成功发送。

收到后的动作

clientMessageID 重查消息,刷新“发送中/失败”状态和重试按钮。

可靠性:失效提示。

关联 API:message.retrymessage.get

6. messageCancelled

原型与载荷

swift
case messageCancelled(Data)

触发条件

cancelMessage(clientMessageID:) 成功取消仍可取消的 Outbox 消息时触发。已经成为 服务端事实的消息不能靠该 API 撤销,应使用 recall。

收到后的动作

重查目标消息;根据查询结果隐藏进度、显示取消状态或删除临时 UI。

可靠性:失效提示。

关联 API:message.cancelmessage.recallmessage.get

7. conversationReadChanged

原型与载荷

swift
case conversationReadChanged(String) // conversationID

触发条件

单会话已读水位、全部会话已读或远端同步的已读状态改变时触发。

收到后的动作

同时重查:

  1. 该会话摘要;
  2. totalUnreadCount()
  3. 页面展示对端已读时,重查 conversationPeerReads

可靠性:失效提示;批量已读可能只需要一次会话列表全量刷新。

关联 API:conversation.mark_readconversation.mark_all_readconversation.total_unreadconversation.peer_reads

8. messageMutated

原型与载荷

swift
case messageMutated(String) // clientMessageID

触发条件

消息编辑、撤回或仅对自己删除完成并更新本地可见投影时触发。

收到后的动作

clientMessageID 时调用 message(clientMessageID:);消息已不可见时重查 时间线。不要只修改当前 Cell 的文本,因为 revision、预览和会话摘要也可能变化。

可靠性:失效提示。

关联 API:message.edit_textmessage.recallmessage.delete_for_self

9. mediaTaskUpdated

原型与载荷

swift
case mediaTaskUpdated(XHIMMediaTaskUpdate)
字段含义
taskID持久媒体任务 ID
state / nativeState已知任务状态和未来原始数值
schemaVersion提示载荷版本

触发条件

上传/下载任务排队、获得授权、传输、校验、退避重试、完成、失败或取消时触发。

收到后的动作

总是调用 mediaTask(taskID:) 获取完整快照。终态为 completed 后才能打开缓存或 确认依赖消息已入队;失败时依据 failureRetryablelastFailureCodenextAttemptAtMilliseconds 展示动作。

可靠性:状态提示会合并;中间进度不保证逐字节到达,终态持久保存在数据库。

关联 API:全部 media.* 任务 API。

10. messageUpserted

原型与载荷

swift
case messageUpserted(XHIMProjectionChange)

触发条件

本地发送、远端同步、历史同步或消息 mutation 使消息投影新增/替换时触发。

收到后的动作

  • conversationID 非空:重查该会话 messages
  • clientMessageID 非空且页面只关心单条:可调用 message
  • requiresFullRequery 或 revision 跳号:重查当前页面的完整时间线。

可靠性:投影失效提示。

关联 API:message.getmessage.listmessage.history

10.1 messageLocallyDeleted

原型与载荷

swift
case messageLocallyDeleted(XHIMProjectionChange)

触发条件

message.delete_localmessage.delete_all_local 在当前设备新增了一条或多条 本地删除 tombstone。这不是服务端删除或跨设备同步事件。

收到后的动作

conversationID 重查当前时间线;当会话 ID 为空时重查当前账号正在 显示的时间线与会话页。不要按旧 IndexPath 直接删 UI 数组。

可靠性:本地 durable projection 失效提示;实际可见数据以重查结果为准。

关联 API:message.delete_localmessage.delete_all_local

11. messageStateChanged

原型与载荷

swift
case messageStateChanged(XHIMProjectionChange)

触发条件

Outbox 或远端回执使消息从 queued/sending 进入 sent、delivered、read、failed、 cancelled 等新状态时触发。

收到后的动作

clientMessageID 重查消息;如果 ID 缺失或 revision 不连续,重查会话 时间线。change.messageState 只用于快速判断刷新优先级,不作为最终状态源。

可靠性:投影失效提示;快速状态迁移可能只观察到较新的提示。

关联 API:message.getmessage.listmessage.retrymessage.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
friendshipsfriendships
groupsgroups
groupMembersgroupMembers(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.connectlifecycle.notify_network_available 和全部 本地查询 API。

15. presenceChanged

原型与载荷

swift
case presenceChanged(XHIMPresenceUpdate)
字段含义
eventID实时事件 ID,用于短期去重
userID状态所属用户
status / nativeStatusonline、away、offline 或未来值
activePlatforms当前未过期 Presence 租约所属端的去重列表
sequence该实时流顺序提示
expiresAtMilliseconds到期时间

触发条件

订阅范围内用户发布在线状态,或实时服务下发状态变化时触发。

收到后的动作

userID 更新内存 UI 状态;到达 expiresAtMilliseconds 必须本地过期为未知/ 离线。它不是持久用户资料,不写入长期业务缓存。

可靠性:瞬时、允许丢失和覆盖;重连后不得继续展示已经过期的值。

关联 API:user.publish_presenceuser.query_presenceuser.subscribe_presenceuser.unsubscribe_presenceuser.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_typinguser.query_typing

17. customSignalReceived

原型与载荷

swift
case customSignalReceived(XHIMCustomSignal)
字段含义
eventID接收事件时的服务端事件 ID;发布完成回执中为空
conversationID / userID会话与发送者
clientSignalID发送方生成的信令 ID
contentType / contentVersion应用自定义合同与版本
payload1...65536 字节的不透明载荷
sequence实时流顺序提示
expiresAtMilliseconds必须清理 UI 状态的最晚时间

只有当前在线会话成员会收到该回调。未知 contentType、不支持的 version 或已过期载荷必须直接忽略;不得回退为聊天消息或计入未读。 该事件允许丢失、覆盖和乱序,不用于订单、OA 审批、支付或审计。

关联 API:user.publish_custom_signal

17.1 accountStateChanged

原型与载荷

swift
case accountStateChanged(XHIMAccountStateChange)
字段含义
eventID服务端账号事件 ID;可用于当次诊断,不作持久幂等键
state / nativeStateactive、suspended、unregistered 或前向兼容 unknown 及原始值
kickedReason / nativeKickedReasonnone、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 时不产生该强类型回调。

收到后的动作

  1. 取消该账号下的普通在途请求,停止自动续凭和无限重试;
  2. 以账号 epoch 为边界销毁页面、数据仓库和事件订阅;
  3. suspended/unregistered 都回到业务登录页;恢复停用账号后也必须 重新签发 Token;
  4. 只用固定 stable code 选择本地可读文案。不查找、不显示管理员 原因,不将它记入分析或崩溃日志。

Facade 按 account epoch 屏蔽退出/关闭后迟到事件,按正整数 revision 屏蔽重复/降序事件。revision == 0 只屏蔽完全相同的状态+原因, 不将其保存为权威最新版本。

18. projectionInvalidated

原型与载荷

swift
case projectionInvalidated(XHIMProjectionChange)

触发条件

Facade 收到未来 schemaVersion、未知 projection kind,或无法安全映射的新载荷 时触发。这是向前兼容保护,不是普通业务错误。

收到后的动作

不要 switch 未知原始值推测业务含义。对当前页面依赖的消息、会话或社交投影 执行完整重查;同时记录 nativeKindschemaVersion 和 SDK 版本供兼容性 排查,不记录消息正文或凭证。

可靠性:保护性失效提示。

关联 API:所有本地查询 API、lifecycle.compatibility

18. reliableBusinessNotificationReceived

公开面 ID:event.reliable_business_notification

载荷

notificationEventIDserviceUserIDoperationIDoccurredAtMilliseconds 和 owned XHIMBusinessNotificationdataJSON == 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。

XHIM 客户端 SDK 与服务端文档