Middleware 与 Event
使用通用 Middleware 和 Event 扩展 Transport、Endpoint、Service 及协同扩展模块。
Middleware 与 Event 是 Collaboration SDK 的通用业务扩展点。身份与权限通过 Middleware 接入。 Middleware 适合在操作执行前进行校验和限制;Event 适合在状态确认后记录变化或触发后续处理。两者都可 用于日志、请求追踪、指标采集和派生数据更新。
两种不同的扩展时机
| Middleware | Event | |
|---|---|---|
| 运行时机 | 一次操作执行过程中 | 状态变化已经确认后 |
| 能否参与或拒绝操作 | 可以 | 不可以 |
| 能否补充调用 Context | 可以 | 读取 Event 携带的 Context 与结果 |
| Listener 失败 | 可能使当前操作失败 | 被隔离,不改变已经确定的领域结果 |
| 典型用途 | 身份、权限、业务校验、日志、计时 | 派生索引、缓存、指标、进程内通知 |
简单判断方式是:需要决定“这次操作能否继续”时使用 Middleware;需要知道“某个状态变化已经发生” 时使用 Event。
Middleware 执行模型
同一 Action 的 Middleware 按注册顺序组成异步链。await next() 进入下一个 Middleware,最内层才
执行 SDK 操作:
service.use("submitChangeset", async (context, next) => { const startedAt = performance.now(); context.customData.startedAt = startedAt; try { await next(); } finally { metrics.observe("collaboration.submit.duration", performance.now() - startedAt); }});Middleware 可以:
- 读取 action-specific request;
- 在
customData中共享当前调用所需的数据; - 在调用
next()前检查输入或查询业务系统; - 在
next()返回后记录耗时或结果; - 抛出
CollabError拒绝预期内的协同操作。
Transport HTTP Middleware 也可以直接结束 HTTP response 并不调用 next()。Service 和 Endpoint
Middleware 若要拒绝操作,应抛出明确错误,而不是静默跳过 next()。
Transport Middleware
transport.use() 注册普通 HTTP Middleware,按注册顺序执行。调用 await next()
进入后续处理,也可以直接结束响应。transport.useUpgrade() 则用于 WebSocket 握手。
例如,先统计 HTTP 耗时,再认证并设置可信 userID,最后注册 Collaboration Endpoint:
transport.use(async (context, next) => { const start = performance.now(); context.response.once("finish", () => { recordHttpDuration({ method: context.incomingMessage.method, route: context.route?.path ?? "unmatched", status: context.response.statusCode, durationMs: performance.now() - start, }); }); await next();});transport.use(async (context, next) => { const user = await authenticate(context.incomingMessage); if (!user) { context.response.statusCode = 401; context.response.end("Authentication required"); return; } context.userID = user.userId; await next();});transport.register(collaborationEndpoint);recordHttpDuration 和 authenticate 由应用提供。Endpoint 使用前序 Middleware 设置的
userID 处理请求;观测 Middleware 注册在认证之前,因此认证拒绝也会被记录。
context.route?.path 是匹配后的完整路由模板,在执行到 Endpoint 时才可用。
通过 response finish 统计响应完成的耗时;路由匹配前的拒绝和未知路径记为 unmatched。
Endpoint Middleware
Endpoint Middleware 参与实时 Session 与 Presence 操作:
| Action | 触发时机 | 常见用途 |
|---|---|---|
connect | Ticket 已消费、Session 已创建,处理 HELLO 前 | 连接策略、补充成员名称与头像 |
joinUnit | Session 加入 Unit Room 前 | Room 准入、Unit 可见性检查 |
receivePresence | 收到已 JOIN Session 的 Presence 后、广播前 | 校验、过滤 Presence |
sendPresence | Presence 发给某个 Room 成员前 | 按接收者过滤 Presence |
endpoint.use("receivePresence", async (context, next) => { if (JSON.stringify(context.payload).length > 8_000) { throw new CollabError("INVALID_REQUEST", "Presence payload is too large"); } await next();});Endpoint 的 session.userID 和 session.customData 来自签发 Session Ticket 的 HTTP request。memberID
由 Endpoint 为当前 WebSocket Session 创建,重连后会变化。
Service Middleware
Service Middleware 参与权威协同数据的生命周期:
| Action | 操作边界 | 常见用途 |
|---|---|---|
readUnitData | 读取 snapshot、block 或 changeset 前 | 读取规则、tenant、审计 |
createUnit | 原子创建 Unit 前 | 类型检查、创建权限、业务规则 |
deleteUnits | 删除批次写入 Adapter 前 | 删除规则、逐 Unit 检查 |
recoverUnits | 恢复批次写入 Adapter 前 | 恢复规则、审计 |
submitChangeset | 逻辑提交进入幂等与 OT 前 | 编辑规则、总大小限制、trace |
applyChangeset | OT 后的 changeset 应用到候选状态前 | mutation 级校验 |
commitChangeset | Adapter 执行 revision CAS 与写入前 | 最终 revision 规则、指标 |
service.use("createUnit", async (context, next) => { await creationPolicy.assertAllowed({ userID: context.userID, unitID: context.request.snapshot.unitID, type: context.request.snapshot.type, }); context.customData.creationPolicyChecked = true; await next();});submitChangeset 每次逻辑提交运行一次。发生 revision 竞争时,applyChangeset 和
commitChangeset 可能以新的 attempt 重复运行,因此这两个阶段的 Middleware 必须可重试,不能
直接发送不可撤销的外部消息或执行一次性扣费。
customData 的作用域
customData 是应用可写的请求级对象,适合保存:
- tenant、trace ID 或请求 logger;
- 本次调用复用的 ACL 查询结果;
- 计时起点或审计标签;
- 当前 Middleware 链共享的业务对象。
普通 HTTP request、Service 调用和 WebSocket Session 拥有不同生命周期。customData 不会自动写入
协同数据库、发送到浏览器或记录日志。需要长期保存的数据必须进入应用自己的存储。
Event 执行模型
Service 使用 on() 注册进程内 Listener,并返回可释放的订阅:
const subscription = service.on("changesetCommitted", async (event) => { metrics.increment("collaboration.changeset.committed", { unitType: String(event.changeset.type), }); localCache.delete(event.changeset.unitID);});subscription.dispose();同一 Event 的 Listener 按注册顺序依次执行并被等待。Listener 抛出的错误会被记录和隔离,不会改变
已经确定的领域结果。例如 changesetCommitted Listener 失败,不会回滚已经写入 Adapter 的 changeset。
因此 Event 适合进程内、可恢复或非关键的后续处理。它不是与数据库 commit 共享的事务边界,也不保证 外部系统一定收到通知。
Core Service Event
| Event | 已确认的事实 | 典型用途 |
|---|---|---|
unitCreated | Unit 及其初始数据已创建 | 更新本地目录缓存、建立派生索引 |
changesetCommitted | Changeset 已确认并推进 revision | 广播、指标、派生索引 |
unitsDeleted | 删除批次已由 Adapter 确认 | 清理进程内关联状态 |
unitsRecovered | 恢复批次已由 Adapter 确认 | 恢复本地派生状态 |
Endpoint 监听同一个 Service 的 changesetCommitted,把 confirmed changeset 广播给当前进程中已经
JOIN 对应 Unit 的 Client。Endpoint 自身还提供 memberLeftUnit Event,用于观察成员显式离开、断线或
Endpoint 释放。
扩展模块拥有独立扩展点
History、Thread Comment 与 Worktree 不自动继承主 Collaboration Service 的 Middleware 或 Event:
- History 提供读取和索引 Middleware;
- Comment 提供 add、reply、edit、delete、solve/list Middleware,以及
commentCommittedEvent; - Worktree 提供生命周期、draft read/submit/commit Middleware,以及创建、状态变化、合入结果等 Event。
应用应分别为实际启用的模块安装规则。主 Service 上的日志、tenant 或权限 Middleware 不会自动覆盖 它们。
接下来可阅读身份与权限,查看如何把已有用户与 ACL 系统 映射到这些通用 Middleware。
你觉得这篇文档如何?