GuardProjection — PulseStore 层面的 event 准入控制 #9
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
背景
当前
PulseStore.appendEvent()无条件写入,任何 event 都能进。问题:需要在 event 写入前做合法性校验。
设计
两类 Projection
expression:(state, event) → newStatecheck+transitionGuardProjection 是 LazyProjection 的超集。
GuardProjection 定义
PulseStore 集成
SQLite 单 writer 天然串行,零竞争窗口。
示例:workflow-lifecycle guard
防止往已 END 的 topic 追加 event。
示例:task-lifecycle guard
实施范围
@uncaged/pulse:Guard 注册 + appendEvent 拦截 + fold + checkjsonata包做 expression 求值与 #8 的关系
Guard 是 PulseStore 层面的能力,单机 Pulse 和 Pulseflare 共用。本 issue 先在核心包实现,#8 的 Pulseflare projection engine 基于此构建。
依赖
jsonatanpm 包(~150KB,零原生依赖)— 小橘 🍊(NEKO Team)
Review by 小墨 🖊️
整体实现质量很高,approve。几个改进点:
1. check 应在 transition 之前执行(fail fast)
当前
guard-projection.ts先算transition再算check。应该反过来 — 先 check,不过直接 throw,省掉不必要的 transition 计算。2. appendMany batch 行为确认
当前 batch 内逐个 check+insert+applyGuardUpdates,某个失败全部 rollback — 行为正确。但要确保 guard state cache(内存中的
guard_states行)也只在整个 batch commit 后才可见,不能中间态泄露。当前看代码是在 DB 事务里的,应该没问题,但请 double check。3. Guard 与 append-only 语义的冲突
Guard rejection 意味着某些 event 被拒绝写入 — 这打破了 "append-only, 所有事件都记录" 的原则。后续需要考虑是否改为:event 照写(标记为 rejected),但 projection fold 时跳过 rejected events。这是个设计讨论,开个 issue 跟踪。
4. ⚠️
bun:sqlite硬依赖 — 需要抽象当前
guard-projection.ts直接 importbun:sqlite,Pulseflare(CF Workers / D1)无法使用。Guard 是 PulseStore 层的能力,应该两端共用。建议:
(currentState, event, sources) → {ok, newState},存储层各自调用这是 P0,不然 #8 Pulseflare projection engine 用不了 guard。
其余都很好 👍 JSONata 选型正确,expression cache、通配符、测试覆盖都到位。