← 返回目录

第 5 卷

第 5 / 6 卷日期:

为什么这么严?因为日志随时要能被重放,重放就是把事件从头再算一遍;函数若读了时钟、改了全局变量,同一份日志算两次会得出两个结果,那日志是唯一依据就成了一句空话。

这也是缓存敢存在的理由:既然同样的输入永远得出同样的输出,算过一次的结果就可以一直用下去,不必每次回头跟日志核对。换句话说,纯函数是缓存的安全前提。

下面写一个我们自己的单元:demo/stats——统计这次对话问了几次、调了几个工具(它是本讲示例脚本 projection-demo.ts 里注册的,官方并没有这个 key)。第一步是声明合

并:在 SessionProjectionMap 这张类型表上给自己占一格。

复制代码 1interface Stats {

2userTurns: numbertoolCalls: number 3}

4

5declaremodule'@deepseek-ai/dsh-session-projection'{
6interfaceSessionProjectionMap{
7'demo/stats':Stats
8}
9}

然后是单元本身:

3toolCalls: z.number(), 4}),

5init: ():Stats=>({userTurns:0,toolCalls:0}),*//空日志时的状态*apply: (
6if(event.type==='tool/call')return{ ...state,toolCalls: state.toolCalls
7returnstate*//与己无关:原样返回同一个引用*
8},
9view: (state:Stats):Stats=>state, *//状态→交给客户端的全量值*stateVersion: 1
10}

注意 key 后面的 as const:少了它,key 会被推断成 string,注册时就对不上类型表里的那一格了。(schema 那行用的是 zod,z 就是它;它描述的是投影值交给客户端之前的形

状,不是事件在日志里的形状。)

注:示例脚本的 apply 开头还多一行 applyCount++,那是为了打印后面那两个计数,与投影机制本身无关。

单元交出去之后,谁来用?框架。写单元的人一行订阅都不写,注册表(ctx.sessionProjections)只订阅一次 session/event,之后每追加一条事件就把

它喂给每个单元的 apply。所以投影不是用的时候才算,而是事件一到就被推着算。

要推得动,会话得是 ctx.sessions.create() 造的活跃会话;Session.create() 造的游离会话不广播,是座孤岛。

跑 npm run start:ch08:projection:

复制代码 1--- ① 一条事件都还没有时的投影 ---

2{"asOfSeq":-1,"values":{"demo/stats":{"userTurns":0,"toolCalls":0}}}
4---②追加5条事件之后---
5{"asOfSeq":4,"values":{"demo/stats":{"userTurns":2,"toolCalls":1}}}
6apply被调用了5次(事件数=5)

三处值得看:

apply 调 5 次,通知只有 3 次。 turn/start、step/start 与这个单元无关, apply 原样返回了同一个 state 引用;注册表用 Object.is 一比,发现没变,下游一点

活都不干。契约写得很死:不关心的事件必须返回同一个引用,与己无关的事件,代价几乎为零。

asOfSeq 是水位线。每段输出那个 {"asOfSeq":…,"values":{…}} 就是一次快照:

values 里一个 key 一个值,是该单元当前的整体值;asOfSeq 则说明它们都算到日志的哪一行(空日志是 -1)。所以你拿到的永远是一个一致的切面;而且给的是全量值,不是

比上次多了一个的裸增量,消费方不必自己维护累加器,漏听一次也不会算错账。

注册本身是一个 effect。第 ③ 段把单元注销,values 立刻变空——投影随之消失。这跟第 01 讲的 Cordis effect 是同一套:卸载干净,不留残余。

至于形状,照着上面那个单元加一格就行:在类型表上声明合并出一个 key,注册一个单元,剩下的驱动、缓存与通知全部由框架接管。

投影不必每次从头算

投影真跑起来,还有一个现实问题:每次都从第一行算起?不必。

算到第 N 条时的状态,可以顺手记成一张便利贴(检查点):算到第几行 + 结果是什么。

下次从便利贴接着算,不用从头翻账本。

但便利贴永远不是权威,它只是条抄近路的捷径,可能陈旧,却绝不能错。所以丢了、脏了、跟当前单元的版本对不上,系统的处理一律相同:把便利贴扔掉,重新翻账本算一遍。正因为

它随时可以扔,写它的时候才敢 fail-soft(失手也不致命):写不进去就写不进去,顶多下次多算几行,绝不会因此给出错误的值。

全站分卷索引 · 6 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷
  5. 5第 5 卷
  6. 6第 6 卷