- 任务调度
- 后端
【免费下载链接】river
The polyglot queue: Fast and reliable background jobs in Go, Ruby, Rust, and JS/TS on Postgres or SQLite.
本篇技术指南围绕 River 仓库中js/工作区的日常开发展开,覆盖从环境准备(Node.js 26 原生 Temporal)、pnpm 命令矩阵、Vitest 测试守卫与属性测试,到 Postgres 集成测试、打包校验、CI 门禁以及八包联合发布的完整链路。读完本文,你将掌握在 River 仓库中为riverqueue(Go、Ruby、Rust、JS/TS 多语言后台任务系统)的 TypeScript 实现做开发、测试、校验与发布的一套可复现工作流,并能对照源码定位每条命令的底层实现。
一、工作区概览与定位
River 是一个多语言后台任务系统,其官方实现以 Go 为核心,并在同一仓库内维护 Rust 与 JavaScript/TypeScript 实现。JavaScript/TypeScript 工作区位于仓库的js/目录,与 Go、Rust 实现并列,相关说明见 js/docs/README.md 与仓库根 README.md。
从工作区结构可以推断其"多包 monorepo"形态:
- 核心包
riverqueue(js/package.json定义了包名、engines.node: ">=26"、exports与sideEffects: false); - 卫星包分别位于
js/migrate、js/driver/pg、js/driver/prisma、js/driver/sqlite、js/worker-threads、js/test、js/cli; - 工作区清单见 js/pnpm-workspace.yaml,其中
packages列出了cli、driver/*、examples/*、migrate、test、worker-threads; - 示例位于
js/examples/*,每个示例含独立的 README 与 package.json,是本文所讲打包门禁的验证对象。
运行工作区命令有两种方式:在js/内直接执行pnpm,或使用 River 顶层 Makefile 中的make lint/js、make test/js、make build/js等目标,它们委托给pnpm -C js。例如Makefile中build/js执行pnpm -C js run build:all,lint/js先构建再依次运行 lint、格式校验与两条类型检查线,test/js则依赖build/js与generate/fixtures。
二、环境准备:Node.js 26 原生 Temporal 是硬前提
River JS 的完整运行时明确要求 Node.js 26 自带的原生 Temporal 实现。开发文档给出了两个验证要点:
安装前检查 Temporal:请使用官方 Node.js 26 构建。部分源码编译的发行版(包括某些发行版包和 Homebrew 包)不含 Temporal,会导致大多数单元测试以
ReferenceError: Temporal is not defined失败。检查方式为:node -p "typeof Temporal" # 必须输出 "object"使用固定版本的 pnpm:工作区通过
package.json的packageManager字段固定 pnpm 版本(当前为pnpm@10.22.0)。.node-version文件(js/.node-version,内容为26)供 fnm、nvm(nvm use $(cat .node-version))以及actions/setup-node选择 Node 26。
此外,对外发布声明文件最低要求TypeScript 6.0(编译目标引用esnext.temporal库)。工作区既用 TypeScript 6 构建,也用 TypeScript-next 预览版做额外类型检查(typecheck:next)。相关版本证据见 js/package.json 的devDependencies(typescript: ^6.0.3、typescript-next: npm:typescript@7.1.0-dev...)。
三、命令矩阵:从构建、测试到打包校验
工作区在 js/package.json 的scripts中定义了全部命令,开发文档列出了完整清单:
pnpm run build # 构建核心包 pnpm run build:all # 构建所有公开包 pnpm run clean:all # 清理所有构建产物 pnpm run fmt # 使用 Prettier 格式化代码 pnpm run fmt:check # 检查格式(供 CI 使用) pnpm run lint # 运行 ESLint pnpm run lint:fix # 运行 ESLint 并自动修复 pnpm run test # 运行单元测试 pnpm run test:coverage # 运行单元测试并输出行/分支覆盖率 pnpm run test:integration # 运行集成测试(需要数据库) pnpm run verify:migrations # 校验生成的迁移文件与哈希 pnpm run migration:legacy # 校验并编译固定的 0.1.0 fixture pnpm run docs:api # 用 TypeDoc 生成 API 参考 pnpm run docs:snippets # 对包 README 中的示例做类型检查 pnpm run api:report # 重新生成 etc/*.api.md 声明报告 pnpm run package:check # 校验 tarball、消费者与示例 pnpm audit # 检查所有依赖安全通告3.1 构建顺序的依赖关系
开发文档强调了一个容易被忽略的次序要求:
- ESLint 的类型感知规则和
typecheck:tests会像卫星包一样通过构建后的声明文件读取riverqueue,因此修改src/后必须先运行pnpm run build; - 单元测试同样通过构建产物导入其他工作区包,且
@riverqueue/worker-threads在线程中运行编译后的处理器模块,所以运行pnpm run test前必须先pnpm run build:all。
这与 js/vitest.config.ts 中的 resolve alias 相印证——开发时的别名仅指向src/index.ts与test/src/index.ts等入口,而跨包导入依赖真实的 dist 产物。
3.2 迁移镜像校验(verify:migrations / generate:migrations)
verify:migrations会把js/migrate/migrations下生成的迁移镜像及其 manifest 与仓库中 River 的Go 权威迁移源逐一比对。从 js/scripts/sync-migrations.mjs 的实现可以看到:
- Postgres 权威源为
riverdriver/riverpgxv5/migration/main,SQLite 权威源为riverdriver/riversqlite/migration/main; - 脚本对每个
\d{3}_.+\.(up|down)\.sql文件计算 SHA-256 摘要,写入migrate/migrations/manifest.json,并支持--check只校验不写盘; - 校验失败场景包括:镜像文件缺失、内容与权威源不一致、存在无权威来源的多余 SQL 文件、manifest 过期。
River 迁移变更后,用pnpm run generate:migrations刷新镜像;Makefile的verify/js-migrations与generate/js-migrations目标分别对两者做了顶层封装。
3.3 打包门禁(package:check)
package:check是发布前最重要的一站式校验,从 js/scripts/check-packages.mjs 的实现看,其动作链包括:
- 用
pnpm pack生成 8 个真实 tarball(riverqueue、@riverqueue/migrate、三个驱动、@riverqueue/worker-threads、@riverqueue/test、@riverqueue/cli); - 用publint严格校验,用Are The Types Wrong(
attw,profile 为node16,并忽略预期的cjs-resolves-to-esm规则,因为 ESM-only 包由下面的require(esm)实测兜底)校验类型分发; - 解包检查:必须且只能包含一个根 LICENSE(与仓库权威 MPL-2.0 文本一致)、不打包测试构建与测试数据、源码映射(sourcemap)中引用的每个 source 都能解析到同一 tarball 内的 TypeScript 源、
@riverqueue/cli的入口具备可执行位; - 依赖检查:除 CLI 外,每个库都把
riverqueue作为精确版本 peer(这样版本倾斜会在安装期以ERESOLVE失败而非运行时静默双实例);@types/*一律是可选的 peer,JavaScript 消费者不会被动安装类型包; - 消费者实测:在干净目录中安装 tarball,确认不会拉入类型包,分别用 ESM
import、CommonJSrequire(esm)、CLI--version、SQLite worker 端到端跑通(含精确 int64 参数exactJsonNumber("9223372036854775807"))、严格 TS6 与 TS-next 消费者编译、以及版本倾斜安装拒绝; - 在消费者目录内以 Node 内置
node --test运行scripts/packed-tests/*.test.mjs(见 js/scripts/packed-tests,含 cli、packages、postgres、sqlite、test-helpers 等),不经过任何转译器或工作区别名; - 构建并运行全部示例;当设置
DATABASE_URL时,Postgres 相关测试在一次性 schema 中运行。package:examples则只跑示例部分。两个命令都不会发布任何内容。
3.4 路径与内容封禁
package:check与migration:legacy会拒绝任何逃逸出包根的归档路径,以及任何包含本地检出目录或主目录路径的打包路径与文本(防泄漏校验逻辑见 js/scripts/package-guard.mjs 中的assertPortableArchivePath与assertNoDeniedContent)。如还需拒绝其他子串(例如未发布的兄弟检出目录名),可通过环境变量RIVER_PACKAGE_DENYLIST传入逗号分隔的、大小写不敏感的额外子串,无需把这些名字提交到仓库。
3.5 API 报告与 README 示例校验
api:report从构建后的包重新生成js/etc/*.api.md声明报告(实现见 js/scripts/api-reports.mjs);CI 中运行的api:check在报告过期时失败。报告对每个声明与成员都包含 TSDoc,因此文档改动会进入 code review。两个命令还会在出现"导出声明引用同包未导出类型"时报错,要么导出该类型,要么在scripts/api-reports.mjs中带理由加入 allow-list(被 allow-list 的类型会出现在报告的独立分节)。docs:snippets对每个可发布包 README 中的 TypeScript 代码块做类型检查(见 js/scripts/check-readme-snippets.mjs)。migration:legacy校验固定的 0.1.0 npm 归档与文档快照(js/fixtures/migration-0.1),并在两条编译器通道上编译旧消费者(见 js/scripts/check-legacy-fixture.mjs)。
3.6 依赖覆盖(overrides)的背景
开发文档记录了三条带有安全通告背景的 overrides(均定义在 js/package.json 的pnpm.overrides):
- Prisma 7.9.1 通过其配置工具链固定了
deepmerge-ts7.1.5,根 overrides 提升到 8.0.1 以携带上游安全修复(GHSA-ggr8-5vv4-36mx),待 Prisma 发布稳定的修复依赖后可移除; - Prisma 7.9.1 还为 MySQL 工具链固定了
mysql23.15.3(本工作区从不使用 MySQL),3.24.4 override 修补了该开发路径上的两个通告(GHSA-3f6p-5ww8-9rcr、GHSA-rgwj-5xj2-c3m3); nanoid3.3.18 override 修补 Vite 开发期 PostCSS 路径上的通告(GHSA-2v37-7h3g-55p8),它仍处于 PostCSS 声明的兼容范围内,可在普通锁解析达到至少 3.3.18 后移除。
判断标准是:只有在pnpm audit --prod不带 override 也保持干净时,才移除对应 override。
四、测试守卫(Test guards):杜绝逃逸错误与句柄泄漏
每个 Vitest 配置都会加载 js/scripts/vitest-setup.mjs(在 js/vitest.config.ts 中通过setupFiles声明)。其守卫逻辑分两条:
- 逃逸错误归因:
beforeAll挂接uncaughtException与unhandledRejection处理器,把逃逸出测试的异步失败收集起来,在afterEach(当前测试结束后)或afterAll(文件最后一个测试之后)以AggregateError抛出让测试失败。这解决了 Vitest 只报告游离错误却不使其失败的痛点。 - 事件循环句柄泄漏检测:文件启动时用
process.getActiveResourcesInfo()记录句柄类型计数(timers、sockets、servers、child processes 等);结束时若某类句柄数量超过初始值即失败。提示开发者:在afterAll中关闭 pools、listeners 与 clients,对故意比测试活得更久的 timers 调用unref()。检查会对"已在关闭中"的句柄最多等待 2 秒(HANDLE_SETTLE_TIMEOUT_MS = 2_000),因为 node-postgres 会在 socket 报告关闭前就 resolvepool.end()。
另外,Vitest 的--detectAsyncLeaks不作为门禁:它会误报测试故意遗留的 pending promise(例如用于演练取消逻辑的永不 settle 的 mock query),且其 async hooks 会让大批量测试超时。
五、属性测试:固定种子下的可复现不变式检查
命名形如*.property.test.ts的文件使用 fast-check 对生成输入检查不变式。从 js/src 与 js/driver/sqlite/src 可以找到 7 个属性测试文件:
- js/src/json.property.test.ts:精确 JSON 编解码;
- js/src/unique-key.property.test.ts:唯一键哈希对照 River 规则的参考编码;
- js/src/query.property.test.ts:不透明、可移植的任务列表游标;
- js/driver/sqlite/src/pagination.property.test.ts:SQLite 键集分页在并列键下的行为;
- js/src/cron.property.test.ts、js/src/periodic.property.test.ts 与 js/src/internal/completion-batcher.property.test.ts:基于模型的命令运行(周期性任务注册表与完成批处理器)。
属性测试随常规单元套件运行,并默认使用固定种子(DEFAULT_FAST_CHECK_SEED = 0x5eed,见 js/scripts/vitest-setup.mjs 的fc.configureGlobal),因此单次运行即可复现。失败时会打印其种子与收缩(shrink)路径;可用FAST_CHECK_SEED复现或探索新的输入空间:
FAST_CHECK_SEED=-1747166622 pnpm test src/json.property.test.ts FAST_CHECK_SEED=random pnpm testFAST_CHECK_SEED必须是安全整数或random,否则 setup 直接抛错。
六、行/分支覆盖率:补充证据而非门禁
pnpm run test:coverage使用 V8 覆盖率运行单元套件,终端输出摘要,并将 HTML 报告写入coverage/(配置见 js/vitest.config.ts 的coverage段:provider 为v8,reporter 为text-summary/html/json-summary,报告目录为coverage)。
文档明确其定位:覆盖率只是补充证据——它只展示哪些代码没有单元测试执行,并不能证明行为与 River 一致;行为一致性由 JavaScript 原生测试与 River Go 的录制 golden 确立。因此它没有阈值,也不是 CI 门禁。
七、集成测试:真实 Postgres 与可扩展的压力测试
集成测试针对带 River schema 的真实 Postgres 运行。准备步骤为:创建一次性测试库 → 构建工作区 → 用本检出生成的精确迁移应用 schema:
createdb river_test pnpm run build:all node cli/dist/bin.js migrate-up \ --database-url "postgres://localhost/river_test"默认连接串为postgres://localhost:5432/river_test,可用TEST_DATABASE_URL覆盖:
TEST_DATABASE_URL="postgres://user:pass@host:5432/mydb" pnpm run test:integration(Makefile中test/js/integration目标即此命令,CI 中它会在 Postgres 14–18 五个版本上运行。)
7.1 多客户端压力测试
集成套件包含一个有界的多客户端对抗性测试,位于 js/driver/pg/src/stress.integration.test.ts。其设计要点(从源码可见):
- 3 个客户端共享一个数据库,每个客户端运行各自的 worker 与事件订阅;
- 每个迭代中,任务从所有客户端并发、以不均等批次插入(单批 1–30 个);
- 迭代中途有一个客户端被优雅替换(
runs[index]?.stop({ mode: "graceful", ... })后重启新成员); - 第二个测试用例里,预先用种子决定每个任务的工作时长与取消延迟,让取消与认领、处理器、完成之间的竞态被系统性演练;
- 每次迭代断言:每个任务至多被处理一次(
invocations.get(id)恒为 1)、到达与其唯一终端事件匹配的终态(completed/cancelled/discarded)、且从不丢失; - 使用确定性 PRNG(mulberry32),种子由
RIVER_STRESS_SEED提供,因此失败的种子可直接复现。
通过环境变量可将其扩展为 soak 运行,并复现失败:
RIVER_STRESS_ITERATIONS=500 RIVER_STRESS_SEED=7 pnpm run test:integration \ driver/pg/src/stress.integration.test.ts默认ITERATIONS=10、SEED=1,每迭代 120 个任务,测试超时预算随迭代数增长(20_000 + ITERATIONS * 3_000毫秒)。
八、从检出目录运行 CLI
执行pnpm run build:all后,可用node cli/dist/bin.js <command>运行工作区的riverqueue命令。需要注意:pnpm 不会把工作区包自身的bin链接进其node_modules/.bin,因此pnpm --filter=@riverqueue/cli exec riverqueue找不到它。例如对一次性数据库做基准测试:
node cli/dist/bin.js bench \ --database-url "postgres://localhost/river_bench" --yes --duration 30s九、持续集成:js.yaml的门禁矩阵
River 的 CI 工作流 .github/workflows/js.yaml 在以下触发条件下运行:修改js/、River 迁移或 SQL、Makefile、conformance 或工作流本身,以及推送js/v*发布标签时。每个 job 都安装js/.node-version选定的官方 Node.js 构建,并且typeof Temporal必须是object,否则失败。
CI job 覆盖(与开发文档及工作流源码一致):
- build:
build:all、typecheck:tests、typecheck:next、verify:migrations(与 Go 权威源比对)、api:check、docs:api、docs:snippets、migration:legacy; - lint:先
build(类型感知 lint 依赖构建声明),再lint、fmt:check、license:check; - packages:在 Postgres 18 服务上先
migrate-up,再package:check(此时DATABASE_URL与RIVER_REQUIRE_POSTGRES=1使打包测试的 Postgres 用例由跳过改为必须通过); - examples:同样在 Postgres 上执行
package:examples; - test:矩阵覆盖
26.0.0(最低支持版本)与当前 Node 26 发布版,先make generate/fixtures生成 conformance fixtures,再build:all与test; - test_integration:矩阵覆盖Postgres 14 到 18,先
migrate-up再test:integration。
9.1 conformance fixtures:跨语言一致性证据
部分单元测试会读取 River Go 实现生成到conformance/testdata的 fixtures(unique keys、protocol values、notification dispatch、retry timing、cron schedules、snooze counting),这些文件同样被 Rust 移植版读取。这些 fixtures不提交到仓库:make test/js会先生成它们,因此运行单元测试需要 Go 工具链;pnpm run test之前需要先在仓库根执行make generate/fixtures。缺失 fixture 会直接使对应测试失败(js/src/conformance.test.ts 中readFixture在ENOENT时抛出提示"运行make generate/fixtures")。
make test/js/conformance只运行这些一致性检查(含两个驱动的通知适配器,无需 Postgres 服务器),对应 Makefile 中pnpm -C js exec vitest run src/conformance.test.ts src/cron.test.ts ...。文档特别说明:涉及重复键或整数键插入顺序的两个原始 JSON unique-key 用例仅限 Rust,因为 JavaScript 对象无法保留这些键的序。
十、发布流程:八个包、单一版本、精确 peer
从开发文档与 js/package.json 可知:八个可发布包是js/package.json(riverqueue)、js/driver下的三个驱动,以及js/migrate、js/worker-threads、js/test、js/cli。它们共享一个版本号,与 Go、Rust 的版本相互独立;示例保持私有并停留在0.0.0。VERSION不带前导v;JavaScript 的 Git 标签形如js/vX.Y.Z。
以下命令均在仓库根执行:
拉取变更与标签,选择下一个 JavaScript 版本(如适用可带 prerelease 后缀),创建发布分支:
git checkout master && git pull --rebase git fetch --tags export VERSION=0.x.y git checkout -b "$USER-js-$VERSION"把所有可发布
package.json的版本设为$VERSION,并更新dependencies、peerDependencies、devDependencies中的精确workspace:引用。库以riverqueue为精确 peer;CLI 直接依赖它。保持私有示例的版本及其workspace:*引用不变,也保留历史js/fixtures/migration-0.1文件。刷新锁文件:pnpm -C js install --lockfile-only把 js/CHANGELOG.md 的
Unreleased条目移入[$VERSION] - YYYY-MM-DD小节,并更新任何带版本号的 README 示例。提交包含 manifest、
js/pnpm-lock.yaml、changelog 与 README 改动的 PR,保持 JavaScript CI 开启并在检查通过后合并。若要在本地验证包归档:make check/js/package合并后,把发布提交拉到干净的检出中,确认版本,只推送 JavaScript 标签:
git checkout master && git pull --rebase test "$(node -p 'require("./js/package.json").version')" = "$VERSION" git tag "js/v$VERSION" -m "release js/v$VERSION" git push origin "js/v$VERSION"在上一步打标后的干净
master检出中本地发布(等其 JavaScript 检查通过后)。使用官方 Node.js 26 与固定 pnpm 版本,并用能发布全部八个包的 npm 账号登录。发布工作流是可选的。本地发布需以--provenance=false禁用 provenance(包内publishConfig.provenance: true的默认值):test "$(git rev-parse HEAD)" = "$(git rev-parse "js/v$VERSION^{commit}")" pnpm login pnpm -C js install --frozen-lockfile pnpm -C js run build:all for package in js js/migrate js/driver/pg js/driver/prisma js/driver/sqlite js/worker-threads js/test js/cli; do (cd "$package" && pnpm publish --access public --no-git-checks --tag latest --provenance=false) || break done发布注意点:
- 需要按需完成 npm 的认证提示;
- 必须逐个发布:pnpm 10.22.0 的递归发布不会把
--provenance=false转发给 npm;且pnpm -C "$package" publish会把额外参数转发给 npm 导致EUSAGE失败,因此要在每个包目录内执行pnpm publish; - 循环按依赖顺序先发布依赖,失败即停;若发生部分发布,先从循环中移除已发布包再重跑;
- prerelease 使用
--tag next而非--tag latest。
八个包全部发布后,为
js/v$VERSION创建 GitHub release,把该版本的 js/CHANGELOG.md 更新说明复制到 release body 中;alpha、beta 与 RC 版本标记为 prerelease。
十一、小结
River 的 JavaScript/TypeScript 工作区把"跨语言语义一致性"作为开发与发布的核心约束:迁移镜像直接镜像 Go 权威 SQL 源并做 SHA-256 比对,唯一键、协议值、cron 调度等以 Go 生成的 conformance fixtures 为基准,打包门禁用真实 tarball 在干净消费者中端到端跑通(含require(esm)与精确 int64),而 Node 26 原生 Temporal 与固定 pnpm 版本则保证了运行时与构建链的确定性。对于希望参与或审计 River JS 实现的开发者,按本文的 Setup → 构建 → 测试 → 校验 → 发布顺序即可完整复现官方流程。
- 任务调度
- 后端
【免费下载链接】river
The polyglot queue: Fast and reliable background jobs in Go, Ruby, Rust, and JS/TS on Postgres or SQLite.
相关推荐
River 开发者指南:Go 工作区测试、lint、sqlc 生成与版本发布全流程
River 开发者指南:Go 工作区测试、lint、sqlc 生成与版本发布全流程 导读 :本文围绕 docs/development.md https://l
任务调度后端Buzz 离线转写教程:本地把会议录音转成 SRT 字幕
Buzz 离线转写教程:本地把会议录音转成 SRT 字幕 一段 90 分钟的访谈录音,你想把其中的口述内容变成能搜索、能引用的文字,又不愿意把音频交给任何在线服
人工智能语音音频本地部署桌面应用Phoenix TypeScript 包开发规范与工作流:js/ 多包仓库的构建、测试与发布指南
Phoenix TypeScript 包开发规范与工作流:js/ 多包仓库的构建、测试与发布指南 导读 Phoenix 的 TypeScript 生态全部收敛在
可观测性AI 评测LLMOpsAI 应用人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考