# @convex-dev/batched-replay 中文文档

规范地址：https://npmcn.com/package/%40convex-dev/batched-replay/

版本：0\.1\.1
npm 元数据更新时间：2026-10-23T21:17:16.401Z
中文译文更新时间：2026-11-10T18:03:04.956Z
软件包许可：Apache-2\.0

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40convex-dev/batched-replay
原文对照：https://npmcn.com/package/%40convex-dev/batched-replay/?lang=original
项目主页：https://github.com/convex/batched-replay#readme
源码仓库：https://github.com/convex/batched-replay

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：336 kB，100 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：0 个
- peer 依赖：react ^18\.3\.1 \|\| ^19\.0\.0，convex ^1\.43\.0
- 首次发布：2026-10-23T20:34:42.259Z
- 最新版本发布：2026-10-23T21:17:16.062Z
- 近 30 天下载量：7625（2026-10-11 至 2026-11-09，npm 官方下载统计）
- 已发布版本数：3（近 12 个月 3 个）

## 中文简介

批次快速变化的值(cursors,笔划,游戏动作等)为少数写出一秒钟并平稳地重放,活活活活活或后生活活活.

## 安装

```sh
npm install @convex-dev/batched-replay
```

也可以使用：`pnpm add @convex-dev/batched-replay`、`yarn add @convex-dev/batched-replay`、`bun add @convex-dev/batched-replay`

## README

# @ convex- dev/ 争吵- 重播

[![npm version](https://badge.fury.io/js/@convex-dev%2Fbatched-replay.svg)](https://badge.fury.io/js/@convex-dev%2Fbatched-replay)

平原Convex上的实时-ish数据. 发送者批次快速变化的值 (cursors, 笔划, 游戏动作) 成一个变体, 每~250 ms; 监视器 每批按原时重放. 它看起来光滑活泼,运行着 少了一点, 花费了一小撮写第二,而不是60。 蝙蝠侠是 保存,所以整个会话可以稍后重播.

**现场演示 :** [https://helpful-wren-102.ap-东南-2.convex. site](https://helpful-wren-102.ap-southeast-2.convex.site/) (cursers,一个共享的绘图板,一个幽灵赛车手和一个电话平板大理石.

找到窃听器了吗? 特质请求? [放在这](https://github.com/convex/batched-replay/issues)。 。 。 .

![Two tabs: tab A draws a smiley, tab B replays each stroke about 300 ms behind, and both Strokes sidebars fill in from onAppend](https://raw.githubusercontent.com/convex/batched-replay/main/docs/drawing-demo.gif)

这个例子的画板用两个分页, 从大约4个写一秒.

**现场演示 :** [https://helpful-wren-102.ap-东南-2.convex. site](https://helpful-wren-102.ap-southeast-2.convex.site/)。 。 。 . 打开它两个标签,或者打开它 `#marble` 用手提电脑用手机引导它.

## 安装

```sh
npm install @convex-dev/batched-replay
```

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import batchedReplay from "@convex-dev/batched-replay/convex.config.js";

const app = defineApp();
app.use(batchedReplay);
export default app;
```

## 显示 API

房间是您选择的字符串(一页,一板,一局). 一个流是发件人的 里面有钥匙.

```ts
// convex/cursors.ts
import { v } from "convex/values";
import { components } from "./_generated/api";
import { exposeApi } from "@convex-dev/batched-replay";

export const { append, latest, history, streams } = exposeApi(
  components.batchedReplay,
  {
    // What one sample is. Checked on every append, and it types the hooks.
    // Leave it out and samples are v.any().
    value: v.object({ x: v.number(), y: v.number() }),
    // How long batches are kept after they end. Leave it out to keep them
    // forever (for replays).
    retentionMs: 10 * 60_000,
    // Throw to stop someone watching a room.
    authorizeRead: async (ctx) => {
      const identity = await ctx.auth.getUserIdentity();
      if (!identity) throw new Error("Unauthorized");
    },
    // Return the stream key this caller writes to. `stream` is what the
    // client asked for; here it's only trusted as a per-tab session id.
    authorizeWrite: async (ctx, { stream }) => {
      const identity = await ctx.auth.getUserIdentity();
      if (!identity) throw new Error("Unauthorized");
      return `${identity.tokenIdentifier}:${stream ?? "default"}`;
    },
  },
);
```

两个回调都得到完整的查询或突变 `ctx`这样他们就可以检查你自己的 表格(这个用户在这个房间里吗? ). 注释 `ctx` 用你的应用程序 `QueryCtx` 或者说 `MutationCtx` 从 `./_generated/server` 用于输入 `ctx.db` 访问.

**每个标签一个流密钥 .** 服务器将每条流的批次都放在结束 一个时间表。 如果两个设备(或两个标签)写出相同的密钥,则它们的批次 相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相接相相接相接相接相接相接相接相接相接相相接相接相接相相接相接相相相接相相接相相接相相接相相接相接相相相接相相接相相相相相相接相相相接相接相接相相相相相相相相相相接相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 做一个 键 用户加一个会话 id 每一个标签选择自己(例如) `crypto.randomUUID()` 保留在 `sessionStorage`另见上文.

`usePlayback`是因为 `self` 所以客户需要知道. 把它建在客户端上 传作为 `stream` (与 `authorizeWrite` 检查 ,或者从您自己的查询返回.

例应用没有登录,所以 `authorizeRead` 允许每个人及其 `authorizeWrite` 信任客户的匿名密钥. 别装船.

从自己的功能中也可以使用类: `new BatchedReplay(components.batchedReplay)` 已经 `append`, (中文(简体) ), `latest`, (中文(简体) ), `history`, (中文(简体) ), `streams` 财务报告和审定财务报表 `clear`。 。 。 . `clear` 没有在 `exposeApi`因为它具有破坏性: 用你自己的支票包住它.

## 将镜像照入自己的表格( 在附录中)

Batches是组件的数据,为回放而制作. 当你的应用想要自己的 耐用的副本 一些东西, `onAppend` 是地方: 画完中风, 游戏圈或检查站、传感器滚出和警报、最后看到的存在、, 节制旗,或安排代理 当某事完成.

```ts
import { v } from "convex/values";
import { exposeApi } from "@convex-dev/batched-replay";
import { components } from "./_generated/api";
import type { MutationCtx } from "./_generated/server";

export const { append, latest, history, streams } = exposeApi(
  components.batchedReplay,
  {
    value: v.object({
      k: v.union(v.literal("down"), v.literal("move"), v.literal("up")),
      x: v.number(),
      y: v.number(),
    }),
    authorizeRead,
    authorizeWrite,
    // Called by append after the batch is stored, in the same transaction.
    onAppend: async (ctx: MutationCtx, batch) => {
      // It runs on every append: return early unless a stroke finished.
      const ups = batch.samples.filter((s) => s.v.k === "up");
      if (ups.length === 0) return;
      for (const up of ups) {
        await ctx.db.insert("penUps", {
          stream: batch.stream,
          seq: batch.seq,
          at: batch.start + up.t, // the sample's server time
          x: up.v.x,
          y: up.v.y,
        });
      }
    },
  },
);
```

`batch` 已经 `room`, (中文(简体) ), `stream`, (中文(简体) ), `seq`, (中文(简体) ), `start`, (中文(简体) ), `end`, (中文(简体) ), `samples` (类型为: `value`和 `dropped`。 。 。 . `samples` 仅存有: `dropped` 主要的样本被忽略了 因为它们是在房间之前被捕获的 最后一次清除。 当什么也没有储存的时候,它就不叫。 绘画演示 `example/convex/drawing.ts` 以这种方式建立整个中风.

规则,最重要的第一:

-   **每个流或每个用户写文件,从不写一个共享的房间文件.** 每个人的附子(大约每分4秒)都会写出同样的文件, 冲突与重试,使写出冲突复出 为避而建. 房间总数,使用 [`@convex-dev/sharded-counter`](https://www.convex.dev/components/sharded-counter) 或者说 [`@convex-dev/aggregate`](https://www.convex.dev/components/aggregate)。 。 。 .
-   **先过滤后提早返回**,如上. (中文(简体) ). 它运行在每个附件.
-   **掷出后附子**,并拖住这个人的溪流 每个人在发件人重试时都会重复 环绕最佳工作 `try`编号 :`catch`,或把它交给 `ctx.scheduler.runAfter(0, ...)`。 。 。 .
-   **没有重复的防守.** 钩子每次存储一次, 在同一笔交易中,Convex最多运行一次突变.
-   **顺序为每条流**,通过 `seq`来自不同溪流的蝙蝠 随便什么顺序.
-   **一个批次不是事件.** 中风会跨越很多批次,所以收集它每个 流,并期望部分:中风没有 `down` (在明确之后,或 当发件人将样品从一分多分年龄时),或 `down` 永远得不到 联合国 `up`。 。 。 .

从 `BatchedReplay` 班级没有钩子: `append` 返回同样的 `{ seq, start, end, dropped }` 你们的镜子在下行也是一样 你的变异.

## 发送

```tsx
import { useSender } from "@convex-dev/batched-replay/react";

const send = useSender(api.cursors.append, {
  room,
  stream: tabId,
  flushMs: 250, // one write per 250 ms while there's anything to send
  onError: (error) => console.warn(error), // it retries, with backoff
  onSend: (batch) => countWrite(), // as each batch goes out, e.g. for a HUD
});
onPointerMove((e) => send({ x: e.clientX, y: e.clientY }));
// Discrete events are never thinned out by sampleMs:
send(value, { keep: true });
```

它冲出地上 `pagehide`。失败的发送保存其样本 之后的重试 `flushMs`超过一分钟的样本 因为服务器不会花更多时间.

## 播放回来

```tsx
import { usePlayback } from "@convex-dev/batched-replay/react";

const { values, stats } = usePlayback(api.cursors, {
  room,
  self: tabId, // your own stream: draw your own input locally, with no delay
  interpolate: true,
});
// values: Map<stream, { x, y }> of everyone active in the last minute.
// stats: { delayMs, bufferedMs, batchesPerSecond, samplesPerSecond }
```

它看着房间最近的100条流出,通过 `maxStreams` (至多 1000) 为更广大的人群.

它在服务器后面演奏一点 默认足够覆盖 后一批货已经运抵(大约330米,冲出250米),从不 超过 `maxDelayMs` (第2条). `onSample(stream, value, time)` 每起火灾 样本,按顺序排列,即使标签被暂时隐藏. 丢失的批次是 从 `history` 并按顺序演奏.

画一幅图,每一下都很重要:

```tsx
usePlayback(api.drawing, {
  room,
  self: tabId,
  // Replay the whole stored history through onSample first, instantly and
  // in order, then carry on live with no gap and nothing twice.
  fromStart: true,
  onSample: (stream, sample) => draw(stream, sample),
  // Someone cleared the room. With fromStart, the (now empty) history is
  // replayed again afterwards.
  onReset: () => clearCanvas(),
});
```

一旦一个房间历史悠久,每批回放就会变得缓慢. 若为 `onAppend` 保存您自己所编集的内容( 绘图演示保存完成) 中风,每条溪流的中风都是从这个开始的。 有过 `onAppend` 也存储每个流的最后一个 `seq`,并从查询中返回副本 在一次交易中读到,所以中指和下指一致:

```tsx
const convex = useConvex();
usePlayback(api.drawing, {
  room,
  self: tabId,
  snapshot: {
    // On start, and again after every clear.
    load: () => convex.query(api.drawing.snapshot, {}),
    // Draw it, and say which batch it covers up to, for each stream.
    apply: (snapshot) => {
      drawSnapshot(snapshot);
      return snapshot.pens; // [{ stream, seq }, ...]
    },
  },
  onSample: (stream, sample) => draw(stream, sample),
  onReset: () => clearCanvas(),
});
```

列出的每个溪流,然后从批次播放 `seq`无论老到哪里, 正在获取它错过的任何 `history`,所以没有画两次和什么都没有 在装入的快照丢失时写入。 抓图没有列出流出 从它的第一批. `apply` 如果房间被清空了就不用打电话了 已装入快照; 它又装入了。 见 `example/convex/drawing.ts` 财务报告和审定财务报表 `example/src/Drawing.tsx`。 。 。 .

## 重播

```tsx
import { useHistory } from "@convex-dev/batched-replay/react";

const replay = useHistory(api.drawing, { room, speed: 10, onSample: draw });
replay.restart(); // load from the start, picking up new batches, and play
replay.pause(); // also play(), seek(ms), setSpeed(n)
// replay.position, replay.playing, replay.finished, replay.error
```

时间比 `maxGapMs` (2 s) 剪短了,所以时间很长 不坐过一个小时一无所有。 1 s 后的页面加载重试失败, 双倍到 10 s, 并显示为 `error`。 。 。 .

这个 `Sender`, (中文(简体) ), `Player` 财务报告和审定财务报表 `HistoryPlayer` 钩子后面的阶级很平淡 类型脚本,导出自 `@convex-dev/batched-replay`如果你没有使用 反应.

## 费用

-   **写作:** 一个 `append` 每个发送者每冲浪,所以大约每人4秒 在他们移动的时候 冲了250毫秒 他们闲着的时候什么也没有.
-   **读取:** 每个观众订阅 `latest` 用于每个活动流。 每个 附录运行一次查询 康维克斯与每个观众分享结果, 但每个查看器的更新都算作一个函数调用:关于发送者x查看者x 4个秒。 一个房间里有10个人动了400秒左右 50秒 约一万. 溪流列表最多每5 s.
-   **虐待:** 客户端可以打电话 `append` 随所喜快. 打开 [`@convex-dev/rate-limiter`](https://www.convex.dev/components/rate-limiter) 内 `authorizeWrite` (说,每个用户每秒10个附件) 对于任何公开.
-   **保留:** 批次保存到 `retentionMs` 在他们结束,然后 cron 每分钟删除一次。 无 `retentionMs` 他们被永远保存,所以 存储由发送的每个样本增长: 一人以60分移动光标 一秒钟的样本大约是每小时5 MB。 永远只保留你所要的 重放,像画.

## 限制

用负载测试测量(一个房间的N人,每人移动60赫兹,250赫兹) 在Convex云层部署时, 2026年10月: 26

| 一个房间里的人 | 写出时间( p50) | 每个观众收到 |
| --- | --- | --- |
| 10个 | 约330毫秒 | 45个KB/s |
| 第25条 | 大约360毫秒 | 120克朗/秒 |
| 第50条 | 约460毫秒 | 240 KB/s (每秒) |
| 100个 | 约1.5 s | 460 KB/s (每秒) |

测试从珀斯到一个美国地区, 所以大约300 ms的每个写作是 腾讯网. 没有错误和任何大小的写冲突:每个人 只写自己的批次。 五十个人搬家很舒服 一次在一个房间。 过去,写慢,发送者自动发送更少, 更大的批量, 和回放更落后, 而不是任何失败. 带宽约为每名观众5KB/s 价值缩小.

## 测试

用 [`convex-test`](https://www.npmjs.com/package/convex-test),登记 构成部分从 `@convex-dev/batched-replay/test`数字 :

```ts
/// <reference types="vite/client" />
import { convexTest } from "convex-test";
import batchedReplay from "@convex-dev/batched-replay/test";
import schema from "./schema";

const modules = import.meta.glob("./**/*.ts");

export function initConvexTest() {
  const t = convexTest(schema, modules);
  batchedReplay.register(t);
  return t;
}
```

导出是使用 TypeScript 源 `import.meta.glob`因此,你的应用需要 `convex-test`, (中文(简体) ), `vitest` 财务报告和审定财务报表 `vite` (关于 `vite/client` 类型) 依赖性.

## 示例

`example/` 已有 URL 散列切换的演示。 光标( C)`#cursors`: 打开它 两个标签并移动鼠标; 滑动器集 `flushMs` 这样你就可以比较 平滑但背后有起伏起伏和代价,高UD开关在3个之间 显示相同批次的方式:跳出( 跳到最新点上),平滑( 有直线) 滑行到最新的点 在一个冲出间隔 和重放批。 绘图 (单位:千美元)`#drawing`:一个共享的板子,让其他人的中风以速度重放 他们被抽走,时间被拖走。 一个新的窗口从应用程序中取出棋盘 拥有 `strokes` 财务报告和审定财务报表 `pens` 桌子,然后继续直播。 赛车`#racer`一个小 每圈都是自己的小溪的赛跑比赛,所以你最好的一圈回来 幽灵通过 `useHistory`. 大理石(`#marble`,与 `#tilt` 电话: 电话的倾斜像光标一样发出,在笔记本电脑上滚出大理石. 见 `example/README.md`。 。 。 .

```sh
npm run dev
npm run dev:frontend
# 100 bot cursors on the cursors demo, for 60 s or until Ctrl-C
npm run crowd -- --bots 100 --flush 250 --seconds 60
```

`npm run crowd` 将每个机器人作为自己的客户端与自己的流密钥连接, 发送通过 `Sender` 像浏览器标签. `--seconds 0` 运行到 Ctrl-C, 财务报告和审定财务报表 `--url` 默认为 `VITE_CONVEX_URL` 内 `.env.local`它需要建筑在 `dist/`,哪个 `npm run dev` 随时更新.

见 `DESIGN.md` 对于它是如何工作的.

## 常见问题

### @convex-dev/batched-replay 是什么？

批次快速变化的值\(cursors,笔划,游戏动作等\)为少数写出一秒钟并平稳地重放,活活活活活或后生活活活\.

### 如何安装 @convex-dev/batched-replay？

在项目目录运行 npm install @convex-dev/batched-replay，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：convex（^1\.43\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @convex-dev/batched-replay 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @convex-dev/batched-replay 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @convex-dev/batched-replay 可以商用吗？

@convex-dev/batched-replay 使用 Apache-2\.0 许可。宽松许可：允许商业使用、修改和再分发，并附带明确的专利授权；分发时需保留许可与 NOTICE 文件，修改过的文件要注明改动。以上为概括，不构成法律意见，请以许可原文为准。

### @convex-dev/batched-replay 的下载量有多少？

根据 npm 官方下载统计，@convex-dev/batched-replay 在 2026/10/11 – 2026/11/09 这 30 天内被下载 7,625 次，日均约 254 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @convex-dev/batched-replay 还在维护吗？

最新版本 0\.1\.1 发布于 2026/10/24（17 天前）；至今共发布 3 个版本，其中过去 12 个月发布了 3 个；首次发布于 2026/10/24。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

