首页
/ Rocket.Chat `chat.syncMessages` 新增 `fromTs` 参数:为 `loadMissedMessages` 迁移补全增量同步窗口

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

2026-09-08 19:39:09作者:戚魁泉Nursing

导读

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


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

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

  1. chat.syncMessages 增加可选的 fromTs 查询参数
  2. 使其可作为已弃用的 loadMissedMessages DDP 方法的迁移替代方案
  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),端点支持的查询参数如下:

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

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.tsgetMessageHistory

  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 过滤,若只是忽略该参数,客户端会拿到超宽结果集而不自知。源码中的注释原话即说明该意图:

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

3.2 校验错误码速查

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

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

4.1 无分页窗口路径 handleWithoutPagination

当同时提供 lastUpdatefromTs 时,getMessageHistoryhandleWithoutPagination(见 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

维度 旧 DDP loadMissedMessages(rid, start) 新 REST GET /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. 变更波及范围与验证线索

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


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

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

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

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391