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 请求。

成功的精确定义由 API 决定。例如:

  • sendText 成功:Outbox 已持久接受消息,不代表已经送达;
  • acceptMediaUpload 成功:媒体任务已持久接受,不代表上传完成;
  • markConversationRead 成功:本地已读水位和待同步事实已更新;
  • 查询成功:返回该调用时刻的本地投影快照。

失败时只依赖稳定字段:

字段用法
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. 共同投影载荷

messageUpsertedmessageStateChangedconversationChangedsocialChangedsyncAppliedprojectionInvalidated 携带 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

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)

触发条件

最后一条消息、未读数、已读水位、置顶、免打扰、草稿、清空/隐藏视图等会话摘要 字段改变时触发。

收到后的动作

重查 conversations。如果页面只展示指定会话,也要用查询结果替换整个会话 ViewModel,避免列表排序和总未读不同步。

可靠性:投影失效提示。

关联 API:全部 conversation.* API,以及发送和消息 mutation API。

13. socialChanged

原型与载荷

swift
case socialChanged(XHIMProjectionChange)

触发条件

好友申请、好友关系、好友备注、黑名单、群、群成员、入群申请或群治理投影改变 时触发。

收到后的动作

socialScope 重查:

socialScope查询
friendRequestsfriendRequests
friendshipsfriendships
groupsgroups
groupMembersgroupMembers(conversationID:)
blocksblocks
groupJoinRequestsgroupJoinRequests
none / unknown重查当前页面依赖的全部社交投影

可靠性:投影失效提示;同一治理操作可能影响多个 scope。

关联 API:全部 relationship.*group.* 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 或未来值
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 未知原始值推测业务含义。对当前页面依赖的消息、会话或社交投影 执行完整重查;同时记录 nativeKindschemaVersion 和 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。

XHIM 客户端 SDK 与服务端文档