Rocket.Chat `chat.syncMessages` 新增 `fromTs` 参数:为 `loadMissedMessages` 迁移补全增量同步窗口
2026/9/9 13:02:51 网站建设 项目流程

Rocket.Chatchat.syncMessages新增fromTs参数:为loadMissedMessages迁移补全增量同步窗口

【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat

导读

本文围绕 Rocket.Chat 仓库中@rocket.chat/meteor@rocket.chat/rest-typings两个包的一项 minor 变更展开:changeset 记录为 REST 端点GET /v1/chat.syncMessages增加了可选的fromTs查询参数,使它可以精确替代已弃用的loadMissedMessagesDDP 方法。读完本文,你将理解fromTs的语义、它与lastUpdate/游标分页的约束关系、底层消息查询实现,以及从 DDP 方法平滑迁移到 REST 端点的完整方案。


1. 变更概要:一次补齐“替代品短板的”补丁式改动

该变更仅涉及一个可选查询参数,却是完成chat.syncMessagesloadMissedMessages迁移闭环的关键一步。按 .changeset/sync-messages-from-ts.md 的描述,变更包含三点语义:

  1. chat.syncMessages增加可选的fromTs查询参数
  2. 使其可作为已弃用的loadMissedMessagesDDP 方法的迁移替代方案
  3. fromTs用于限定同步窗口,必须与lastUpdate一起使用;如果与游标分页参数(next/previous)同时发送,将直接返回错误而非静默忽略

从中可以读出设计意图:fromTs负责“从哪里开始”的下界约束,lastUpdate负责“何时之后发生了变化”的上界判定,两者配合才能完整还原loadMissedMessages(rid, ts)的“拉取指定时间点之后的可见消息”这一增量同步语义。


2. 认识宿主端点:GET /v1/chat.syncMessages

在深入fromTs之前,先回顾它挂载的宿主端点。REST 路由定义在 apps/meteor/server/api/v1/chat.ts,对应 TypeScript 客户端类型与方法注册在 packages/rest-typings/src/v1/chat.ts。

2.1 请求参数(查询字符串)

根据ChatSyncMessages类型与 AJV 校验 Schema(packages/rest-typings/src/v1/chat.ts),端点支持的查询参数如下:

参数类型必填说明
roomIdstring目标房间 ID,Schema 中唯一required
lastUpdatestring(日期时间)增量同步的“最近同步时间”;仅返回该时间之后更新的消息/删除记录
fromTsstring(iso-date-time本文新增参数,用于限定消息的起始时间窗口下界
nextstring向前游标(与type配合做游标分页)
previousstring向后游标(与type配合做游标分页)
type'UPDATED' \| 'DELETED'游标分页时指定拉取“更新消息”或“删除记录”
countnumber单页条数(游标分页场景生效,服务端默认值见下文)

fromTs在 Schema 中被标注为format: 'iso-date-time'的字符串,与lastUpdate一样,服务端在解析后会转为Date实例再参与查询(见下一节)。

2.2 成功响应结构

端点内部校验通过isChatSyncMessagesProps(AJV 编译产物),响应体 Schema 定义在 apps/meteor/server/api/v1/chat.ts,核心载荷为:

{ "success": true, "result": { "updated": ["…消息对象数组(IMessage)…"], "deleted": [{ "_id": "…", "_deletedAt": "2024-…" }], "cursor": { "next": null, "previous": null } } }
  • updated:按需返回的已更新消息数组;
  • deleted:被软删除的消息记录数组,每项只含_id_deletedAt
  • cursor:仅在游标分页路径下出现,指向next/previous下一页的游标。

3.fromTs的核心语义与校验规则

fromTs是一个窗口下界约束:它并不取代lastUpdate,而是在其后追加一层时间过滤。这一点可以从 apps/meteor/server/api/v1/chat.ts 的 action 实现看得非常清楚:

const { roomId, lastUpdate, fromTs, count, next, previous, type } = this.queryParams; // roomId 缺失直接报错 if (!roomId) { throw new Meteor.Error('error-param-required', 'The required "roomId" query param is missing'); } // lastUpdate 与 type 至少要给一个 if (!lastUpdate && !type) { throw new Meteor.Error('error-param-required', 'The "type" or "lastUpdate" parameters must be provided'); } // lastUpdate 必须是合法日期 if (lastUpdate && isNaN(Date.parse(lastUpdate))) { throw new Meteor.Error('error-lastUpdate-param-invalid', 'The "lastUpdate" query parameter must be a valid date'); } const getMessagesQuery = { ...(lastUpdate && { lastUpdate: new Date(lastUpdate) }), ...(fromTs && { fromTs: new Date(fromTs) }), // ← fromTs 被显式解析为 Date 后透传 ...(next && { next }), ...(previous && { previous }), ...(count && { count }), ...(type && { type }), };

随后把getMessagesQuery交给公共的历史消息服务函数getMessageHistory(roomId, this.userId, getMessagesQuery)

3.1 三条硬性规则

真正的参数组合约束位于 apps/meteor/server/publications/messages.ts 的getMessageHistory

  1. fromTs必须搭配lastUpdate:若fromTs && !lastUpdate,直接抛出error-fromTs-requires-lastUpdate(“The 'fromTs' parameter can only be used together with 'lastUpdate'”);
  2. lastUpdate不能与游标共用:若(next || previous) && lastUpdate,抛出error-cursor-and-lastUpdate-conflict,游标分页走的是另一条独立路径;
  3. nextprevious互斥:两者同时出现抛error-cursor-conflict

规则 1 与规则 2 叠加,实际效果正如 changeset 所说:fromTs+lastUpdate走无分页的窗口同步路径;fromTs与游标分页(next/previous/type)同时出现时一定会被拒绝——因为游标路径并不执行fromTs过滤,若只是忽略该参数,客户端会拿到超宽结果集而不自知。源码中的注释原话即说明该意图:

fromTsonly bounds the query on thelastUpdatepath; neither cursor pagination nor the channel history fallback honors it, so accepting it there would silently widen the result set.

3.2 校验错误码速查

错误码触发场景
error-param-requiredroomId,或lastUpdate/type均未提供
error-lastUpdate-param-invalidlastUpdate无法被Date.parse解析
error-fromTs-requires-lastUpdate提供了fromTs但未提供lastUpdate
error-cursor-and-lastUpdate-conflictnext/previouslastUpdate同时出现
error-cursor-conflictnextprevious同时出现
error-type-param-required使用next/previous时未提供type
error-type-param-not-supportedtype不是UPDATEDDELETED

4. 源码级原理:fromTs在底层查询中如何生效

4.1 无分页窗口路径handleWithoutPagination

当同时提供lastUpdatefromTs时,getMessageHistory走 handleWithoutPagination(见 apps/meteor/server/publications/messages.ts 的分支调度),核心实现是两条并行查询:

export async function handleWithoutPagination(rid: IRoom['_id'], lastUpdate: Date, fromTs?: Date) { const options: FindOptions<IMessage> = { sort: { ts: -1 } }; const [updatedMessages, deletedMessages] = await Promise.all([ Messages.findForUpdates(rid, { updatedAt: { $gt: lastUpdate }, minTs: fromTs }, options).toArray(), Messages.trashFindDeletedAfter( lastUpdate, { rid, ...(fromTs && { ts: { $gte: fromTs } }) }, { projection: { _id: 1, _deletedAt: 1 }, ...options }, ).toArray(), ]); return { updated: updatedMessages, deleted: deletedMessages }; }

逐条拆解其过滤条件:

  • updated(消息正文更新)Messages.findForUpdatesupdatedAt > lastUpdate为主条件,minTs: fromTs作为下界过滤,即只返回“最近同步时间之后更新过、且消息时间戳不早于fromTs”的消息;
  • deleted(软删除记录)Messages.trashFindDeletedAfter首先限定_deletedAt > lastUpdate(最近同步之后发生的删除),再叠加ts >= fromTs过滤(该消息本身必须位于同步窗口内)。

两条查询使用相同的sort: { ts: -1 }与 Promise.all 并行执行,这正是lastUpdate负责“时间上限侧变化”、fromTs负责“消息自身时间下界”的精确落地,也解释了为什么二者必须成对出现。

4.2 游标路径为何“拒绝”而非“忽略”

与上述窗口路径并列,handleCursorPagination 处理type+next/previous的翻页场景:它按type分别用updatedAt游标或_deletedAt游标在消息表/回收站表中取页,本身并不接受“消息时间戳下界”概念。若客户端在翻页时附带fromTs,服务端无法在不破坏游标语义的前提下应用该过滤,于是通过“fromTs要求lastUpdate+lastUpdate与游标互斥”的组合规则在 apps/meteor/server/publications/messages.ts 直接报错,避免数据悄悄不完整。


5. 迁移视角:从loadMissedMessageschat.syncMessages

5.1 旧 DDP 方法为何需要被替代

被替代的 DDP 方法定义在 apps/meteor/server/meteor-methods/messages/loadMissedMessages.ts,核心逻辑如下:

Meteor.methods<ServerMethods>({ async loadMissedMessages(rid, start) { methodDeprecationLogger.method('loadMissedMessages', '9.0.0', '/v1/chat.syncMessages'); check(rid, String); check(start, Date); const fromId = Meteor.userId() ?? undefined; if (!rid) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getUsersOfRoom' }); } if (!(await canAccessRoomIdAsync(rid, fromId))) { return false; // 无权限时返回 false } return Messages.findVisibleByRoomIdAfterTimestamp(rid, start, true, { sort: { ts: -1 }, }).toArray(); }, });

该文件本身就是迁移信号的载体:方法体第一行便通过methodDeprecationLogger.method(...)登记弃用(日志引用版本'9.0.0',并明确指引替换路径为'/v1/chat.syncMessages')。它返回false | IMessage[]:无房间访问权限时返回false,否则返回“该房间start之后所有可见消息、按ts倒序”。其身份校验位于 apps/meteor/server/meteor-methods/index.ts,客户端的旧调用封装则见 apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts。

权限语义差异提醒:旧 DDP 方法在无权限时静默返回false,而新 REST 端点走canAccessRoomIdAsync校验失败会返回错误响应(见 apps/meteor/server/publications/messages.ts),迁移时需要相应调整失败处理逻辑。

5.2 迁移前后对照

旧实现本质上是「给定起始时间戳start,全量拉取其后的可见消息」。要在 REST 世界中还原这一行为,需要把“起始下界”交给fromTs、把“增量同步时间点”交给lastUpdate

维度旧 DDPloadMissedMessages(rid, start)新 RESTGET /v1/chat.syncMessages
房间标识方法参数rid查询参数roomId
起始下界参数start(Date)fromTs(iso-date-time 字符串)
增量判定点隐含为“当前/最近拉取点”lastUpdate
返回形态false(无权限)或IMessage[]{ updated, deleted, cursor }
补充能力可额外获得删除记录(deleted)与游标分页

5.3 可运行的调用示例

以 curl 形式发送(需携带登录后的身份凭据):

# 拉取 roomId 中、从 2024-06-01T00:00:00Z 起、且自 lastUpdate 之后发生变化的消息与删除记录 curl -G 'https://your-rocketchat.example/api/v1/chat.syncMessages' \ -H "X-Auth-Token: YOUR_AUTH_TOKEN" \ -H "X-User-Id: YOUR_USER_ID" \ --data-urlencode 'roomId=GENERAL_ROOM_ID' \ --data-urlencode 'lastUpdate=2024-06-10T00:00:00.000Z' \ --data-urlencode 'fromTs=2024-06-01T00:00:00.000Z'

fromTs但遗漏lastUpdate(或同时携带next/previous)的请求,将分别被error-fromTs-requires-lastUpdateerror-cursor-and-lastUpdate-conflict/error-fromTs-requires-lastUpdate拒绝。客户端侧的既有接入可参考 useLoadMissedMessages.ts 的 REST 调用方式(sdk.rest.get('/v1/chat.syncMessages', …)),其配套测试 useLoadMissedMessages.spec.ts 验证了对同一端点多次调用时roomId/lastUpdate等参数的组装,可作为自定义客户端迁移的最小参照。


6. 变更波及范围与验证线索

  • 包与版本节奏:changeset 中标注@rocket.chat/meteor: minor@rocket.chat/rest-typings: minor,属于向后兼容的能力增强——fromTs为可选参数,旧调用方不受影响,ChatSyncMessages类型与 Schema 同步更新。
  • 服务端路由:apps/meteor/server/api/v1/chat.ts(端点编排与响应归一化)。
  • 类型契约与校验:packages/rest-typings/src/v1/chat.ts(ChatSyncMessages/ChatSyncMessagesSchema/isChatSyncMessagesProps)。
  • 底层历史服务:apps/meteor/server/publications/messages.ts(handleWithoutPagination/handleCursorPagination/getMessageHistory,含全部组合校验与注释说明)。
  • 被替代方法:apps/meteor/server/meteor-methods/messages/loadMissedMessages.ts(弃用登记与旧实现)。

fromTs行为边界做最直接验证的方式,是阅读getMessageHistory中 参数组合校验 的注释与抛错分支:它们既是运行时行为,也是该参数的权威设计文档。


7. 小结:什么时候该用fromTs

  • 适合使用:客户端希望从某个历史时间点起做“增量补拉”,即原loadMissedMessages场景——请使用lastUpdate+fromTs的组合,一次请求同时拿回该窗口内的消息更新与删除记录;
  • 禁止使用:任何携带next/previous/type的游标翻页请求,服务端会直接报错以阻止静默扩宽结果集;
  • 无需使用:仅需“最近同步之后的所有变更”时,单传lastUpdate即可,fromTs是可选的额外收紧条件。

一言以蔽之:fromTschat.syncMessages从“只能表达最近增量”进化为“可表达任意起点的时间窗同步”,从而完整接住loadMissedMessages的迁移需求——这正是本 changeset 的价值所在。

【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询