Runtime semantics
**Normative** rules for complex Flows: same-frame ordered scan of children, runBranch on entry, scope push/pop. Use this chapter when same-frame replies or nested logout behave unexpectedly.
本节为 implement 唯一权威;与上文冲突时以本节为准。
连接状态
每 WebSocket 连接(Flow 命中后)维护:
- scopeStack: Frame[] // 栈底为 root
- vars: Record<string,string> // 连接级变量
- timers: Timer[] // 每项绑定 scopeFrameId
- phase: open | active | closing | closed
Frame = { id: string, // 稳定 nodeId(compile 分配) astNodeId: string, // 进入该帧的条件/返回 AST 节点 name?: string, // ~auth;root 无 name active: boolean, // false 时其 loop 不触发 enteredAt: number // 调试用 }
连接建立序列
- WS 握手完成,Flow match 命中
- push root Frame { active: true }
- 扫描 root 下 直接
--@loop节点 → registerTimers(见「--@loop注册时机表」) - 触发
--@open(若存在):执行 open 分支(返回 + scope 进入,见 17.4) - vars.$connectionId ← 分配;$now 等在读取时求值
匹配 client 文本帧 evaluateMessage(text)
- 若 phase=closing|closed → return
- capture 本帧(按 # capture)→ merge into vars
- 全局 !~ 候选 popMatches(优先于 entry,见步骤 5)
- 在栈顶 匹配上下文 子树内,源序深度优先 选取 entry 条件 entryNode:
- 第一条 自身匹配 且 从上下文根到该节点路径上所有消息条件均匹配 的
--节点(不含--@生命周期作 entry,生命周期走 §17.7) - 带
!~name的消息条件 不参与 entry 选取(仅步骤 3 popMatches) - 若未命中且 # on-unmatch root → 临时将匹配上下文视为 root 子树重试步骤 4
- 若仍未命中 → 步骤 6
- 第一条 自身匹配 且 从上下文根到该节点路径上所有消息条件均匹配 的
- 裁决(Normative):
- 若 popMatches 非空 → 取源序最先 → popScope(name) → runBranch(popNode, popOnly) 或仅 pop 无返回(见 6.9.3);结束
- 否则若 entryNode 存在 → runBranch(entryNode, enter)(§17.4);结束
- 否则 → 未命中(# on-unmatch 等)
runBranch(node, mode?)
mode 枚举(Normative):
| mode | 典型调用 | 步骤 2 扫描发返回 | push 帧 | 移入 scope | register loop |
|---|---|---|---|---|---|
enter |
entryNode、--@open |
是 | 是(3) | 是(4) | 是(5) |
scan |
步骤 2b 递归子 -- |
是 | 否 | 否 | 否 |
popOnly |
popMatches(§17.3、6.9.3) | 是 | 否 | 否 | 否 |
默认 enter。scan / popOnly 仅执行步骤 2(及步骤 7 !close);不 push、不移入子树、不 register loop。
共享累加器:同一 evaluateMessage 内,enter 与其递归 scan 共用 lastReturnWithChildren(步骤 2a 源序覆盖),供 enter 步骤 4 移入 scope。
- anchorTime ← 本帧 enter 根 run 起点(scan/popOnly 继承同一 anchorTime,§5.1 delay 锚点)
- 对每个直接子节点(源序):
a. 返回行 → 按 §5.1 发出;更新 delay 链;若该返回行下存在缩进子条件,更新共享
lastReturnWithChildrenb. 消息条件--…→ 若本帧 也匹配 → runBranch(该子节点, scan)(递归) c. 生命周期--@…→ 本步不发出 client 返回(loop 在 enter 步骤 5 注册) - 仅 mode=enter:push Frame { name from node 的 ~enter suffix if any, active: true };无 ~enter 仍 push 匿名帧
- 仅 mode=enter:移入 scope — 下一帧匹配上下文 =
- 若
lastReturnWithChildren非 null → 该 返回节点 之子树 - 否则 → node 整棵子树
- 若
- 仅 mode=enter:activateTimers — 对新帧匹配上下文中 已可达 的
--@loopregister(见「--@loop注册时机表」) - 若本 run(enter/scan/popOnly 任一步骤发出的返回)含 !close → 进入 closing,清空栈与 timers
生命周期 --@open 等:由 §17.7 调用 runBranch(node, enter),语义同上。
(implement 别名:executeCondition(entry) → runBranch(node, enter);递归子条件 → runBranch(node, scan)。)
popScope(name?)
- 若 name 给定:pop 直到移除 第一个 name 匹配的帧(含其上方所有子孙帧,即该帧到栈顶整段弹出)
- 若 name 未给定(
--@close等):clear 全部 - 每个被 pop 的 frameId → cancel 绑定 timers
- pop 不 清除 vars
--@loop 调度
onRegister(loopNode, frameId):
delay0 = (# loop immediate on) ? 0 : dur
schedule timer at now+delay0, period=dur
onTick(loopNode):
if frame(frameId).active → 仅发出 loop 节点下返回行(+delay);不走 evaluateMessage、不 push scope、不递归 scan 子 --
onPop(frameId) → cancel all timers where timer.frameId == frameId or frameId 被 pop 的子孙
生命周期事件(简要)
| 事件 | 动作 |
|---|---|
| --@open | runBranch(openNode, enter);可带 ~session push |
| --@ping | 全局;不 push;走 --@ping 返回或 # ping auto |
| --@close | pop 全部;可选最后一帧;phase=closed |
| client text | evaluateMessage |