3.4 依赖方向
学习目标
完成本节后,你将能够:
- 区分 package import 依赖和运行时通信依赖。
- 判断一个抽象应该放在
web、worker、packages/shared还是ee。 - 理解为什么
shared不能 import 上层 package。
3.4.1 当前 package 依赖图
这张图只表示 TypeScript import 方向。它不表示运行时调用方向。
实际运行时里,web 和 worker 不应该直接互相调用函数。它们通过两类边界连接:
- 类型边界:
packages/shared里的 queue payload、domain schema、filter/query contract; - 状态边界:Redis/BullMQ、S3/blob、Postgres、ClickHouse。
3.4.2 编译时依赖和运行时通信不是一回事
以 ingestion 为例:
这里:
webimport@langfuse/shared,用同一个IngestionEventschema;worker也 import@langfuse/shared,用同一个 payload type;web没有 importworker;worker没有 importweb;- 真实数据通过 S3 和 Redis 流动。
这就是 Langfuse 的核心边界:共享契约,不共享运行时实现。
3.4.3 为什么 shared 不能依赖上层
packages/shared 位于依赖图底部。它不能 import web、worker 或 ee,原因不是形式主义,而是为了防止三类问题:
| 问题 | 如果 shared 依赖上层会怎样 |
|---|---|
| 循环依赖 | web -> shared -> web 会让构建、测试和 tree-shaking 变复杂。 |
| 运行时污染 | worker 可能被迫加载 Next.js、React、session 逻辑或浏览器代码。 |
| 契约不稳定 | shared contract 会混入某个页面或 processor 的局部假设。 |
一个规则可以帮助判断:shared 里应该出现“跨边界稳定名词”,不应该出现“某个入口的局部流程”。
3.4.4 抽象放置决策表
| 改动对象 | 应放位置 | 原因 |
|---|---|---|
| React component local state | web/src/features/... | 只服务 UI。 |
| UI 内部 API router | web/src/server/api 或 feature server | 依赖 session、Next.js、tRPC context。 |
| Public API route wrapper | web/src/features/public-api/server | 依赖 Next.js request/response 和 API key auth。 |
| Public API request/response schema | web/src/features/public-api/types + fern/apis/** | 外部 contract,需要文档和 SDK 对齐。 |
| Queue payload | packages/shared/src/server/queues.ts | producer 和 consumer 跨进程共享。 |
| Redis queue class | packages/shared/src/server/redis/** | producer 和 worker 都可能创建/解析队列。 |
| Ingestion event/domain schema | packages/shared/src/server/ingestion/** 或 domain/** | request side 和 worker side 都要理解。 |
| Worker processor | worker/src/queues/** | 只在 worker 运行。 |
| ClickHouse query builder | packages/shared/src/server/queries/** | web/API/worker 可能共享 query 语义。 |
| ClickHouse write batching | worker/src/services/ClickhouseWriter | 当前执行在 worker 内,处理批量写入策略。 |
| Enterprise feature implementation | ee/** 或 web/src/ee/** | 商业能力,但依赖基础 shared contract。 |
3.4.5 判断是否该上升为 shared
把一个 helper 放进 shared 之前,先问:
- 它是否会被
web和worker同时 import? - 它是否表达 Redis job、S3 key、ClickHouse row、Public API response 这类跨边界数据?
- 它是否包含 tenant scope、权限、filter operator、field mapping 这类系统规则?
- 它是否需要在 rolling deploy 中保持兼容?
- 如果它变更,是否至少有两个运行时会受到影响?
如果这些问题大多是 “yes”,它更像 contract。否则它可能只是局部 helper。
3.4.6 依赖方向和测试策略的关系
依赖方向也决定了验证范围:
| 改动位置 | 风险面 | 验证倾向 |
|---|---|---|
web 局部 component | UI 行为和浏览器渲染 | targeted component/test + browser review。 |
worker processor | job retry、side effect、吞吐 | targeted worker test。 |
packages/shared queue/schema/query | web 和 worker 同时受影响 | lint + 至少一个 web 侧和一个 worker 侧回归。 |
fern/apis | 外部 API contract | API tests + Fern regeneration。 |
| Prisma/ClickHouse schema | 数据迁移和读写同构 | db generate + migration/query/writer 回归。 |
shared 改动不是因为代码更底层就更安全,恰恰相反,它的 blast radius 更大。
3.4.7 常见误区
| 误区 | 正确理解 |
|---|---|
web 能用的东西都放 shared | 只有跨运行时或跨 contract 的东西才放 shared。 |
| worker 可以直接 import web service | worker 应通过 shared service/contract 或自己的 service 实现。 |
| queue payload 加字段只是类型改动 | 队列里可能已有旧 job,consumer 要兼容。 |
ee 可以绕过 shared | Enterprise 也应建立在同一基础 contract 上。 |
| 手写 SQL 更快 | 可能绕过 tenant filter、field set、query tag 和 ClickHouse 排序优化。 |