在 iii 中让函数随状态变化自动执行:Reactive State Pattern 完整实战指南
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
Reactive State Pattern 是 iii 中一类"事件驱动"的集成模式:当某个函数的触发条件是某份状态的变更(而不是一次请求或一个定时任务)时,把函数绑定到状态 Worker 所发布的state触发器上,引擎便会在被监听 key 或 scope 每次变化时自动调用该函数。本文将先讲清模式的定义与适用场景,再基于仓库源码逐层拆解触发器配置、事件载荷、匹配规则与端到端示例,让你能在自己的 Worker 中直接落地"数据一变、逻辑即跑"的响应式工作流。
模式是什么:把"数据变了"变成一等触发条件
在 iii 中,函数(Function)的执行由触发器(Trigger)驱动。常见的触发方式有两类:
- 请求驱动(on demand):HTTP 端点被调用、消息被消费时才执行;
- 调度驱动(on schedule):按 cron 表达式在固定时间执行。
Reactive State Pattern 则提供第三种语义:状态驱动(on state change)。当函数关心的不是"谁调用了它",而是"某份数据发生了变化"时,不需要任何人显式调用它——引擎会在被监听的状态发生变化时自动触发。
从源码看,这是 iii 内置触发器体系中的一等公民。在 engine/src/trigger_formats.rs 中,引擎为state触发器定义了独立的配置结构与调用载荷结构;在 engine/src/trigger.rs 中,state与state触发器类型被显式注册到触发器目录中,与http、cron、queue、subscribe等内置类型并列。
该模式也是 iii 官方顶层文档中单独成篇的架构模式之一,见 docs/patterns/reactive-state-pattern.mdx。
什么时候使用该模式
当业务动作在概念上是"每当这份数据变化时,就做这件事"("whenever this data changes, do this"),而不是"按调度"或"按需"时,就适合使用该模式。典型例子:
- 重建派生索引:当源状态变化时,重新计算并写回派生的索引数据;
- 扇出通知:当用户记录更新时,向相关服务或订阅者广播变更通知;
- 跨 scope 传播计算值:当某组输入变化时,把计算结果传播到另一个 scope 中。
反面情况也很清晰:如果动作只关心"定时执行"(用cron触发器)或"有人来调用"(用 HTTP 等请求型触发器),就不属于该模式的应用范畴。
模式的三段式结构
该模式的实现结构由三个角色组成:
- 状态 Worker 持有源状态(source-of-truth):状态以
scope(命名空间)+key的方式组织,任何 Worker 都可以通过state::set/state::update等内置函数读写; - 某个 Worker 中的函数负责处理变化:计算派生值、发送通知等,函数代码与普通函数完全一致;
- 状态 Worker 广告的
state触发器绑定函数:触发器声明它关心哪个scope/key,引擎在每次变化时评估并触发绑定函数。
关键点在于:函数代码本身没有任何特殊之处,唯一的差异只体现在触发器注册这一步。同一份函数逻辑,既可以挂在 HTTP 触发器下按需调用,也可以挂在state触发器下随状态变化自动执行。
状态 Worker 的读写表面:state::*内置函数
Reactive State Pattern 的"数据源"由状态 Worker 提供,它暴露一组state::*内置函数作为读写接口(各 SDK 中均有类型化封装,见 sdk/packages/node/iii/src/state.ts 的IState接口):
| 函数 | 作用 | 触发的状态事件 |
|---|---|---|
state::set | 设置(创建或覆盖)一个值 | 键不存在时触发state:created,存在时触发state:updated |
state::get | 读取一个值 | 无 |
state::delete | 删除一个值 | 触发state:deleted |
state::update | 用一组操作原子更新值 | 按键是否存在触发state:created/state:updated |
state::list | 列出 scope 内所有值 | 无 |
state::list_groups | 列出所有含数据的 scope | 无 |
其中state::set的输入为{ scope, key, value },返回值包含old_value(旧值,键不存在时为null)与new_value(新写入值)。state::update支持set、merge、increment、decrement、append、remove等原子操作序列,可以避免"读-改-写"竞态。
state触发器配置:scope 与 key 的匹配规则
state触发器的配置结构在引擎侧定义于 engine/src/trigger_formats.rs:
pub struct StateTriggerConfig { /// State scope to watch (exact match filter) pub scope: Option<String>, /// State key to watch (exact match filter) pub key: Option<String>, /// Optional function ID to evaluate before invoking handler pub condition_function_id: Option<String>, }三个字段的匹配语义如下:
| 字段 | 类型 | 语义 |
|---|---|---|
scope | string(可选) | 精确匹配。仅当状态变化发生在该 scope 内时触发;省略时匹配所有 scope |
key | string(可选) | 精确匹配。仅当状态变化发生在该 key 上时触发;省略时匹配所有 key |
condition_function_id | string(可选) | 条件函数。引擎先把状态事件交给它评估,返回false时不调用处理器函数 |
注意scope与key都是精确匹配(exact match)过滤:配置{ scope: "users" }会监听users下所有 key 的变化;配置{ scope: "users", key: "profile" }则只监听users/profile这一个键的变化。两者都省略时,该触发器会响应所有scope 下的所有状态变化——除非确有全局需求,否则建议至少限定scope,避免无关变更引发大量无效调用。
Rust SDK 提供了对应的构建器风格 API(sdk/packages/rust/iii/src/builtin_triggers.rs):
let config = StateTriggerConfig::new() .scope("users") .key("profile") .condition("conditions::emailChanged");状态事件载荷:处理器收到的数据
当触发器被匹配并触发时,处理器会收到一个结构化的状态事件对象。引擎侧的线格式定义于StateCallRequest(engine/src/trigger_formats.rs),SDK 侧的类型化封装见StateEventData(sdk/packages/node/iii/src/state.ts):
| 字段 | 类型 | 说明 |
|---|---|---|
type | string | 固定为"state" |
event_type | enum | 变更类型:"state:created"、"state:updated"、"state:deleted" |
scope | string | 发生变更的 scope |
key | string | 发生变更的 key |
old_value | any | 变更前的值;新建键时为null(删除事件时携带被删值) |
new_value | any | 变更后的值;删除事件时为null |
三种事件类型在引擎与各 SDK 中保持一致枚举定义(StateEventType),例如 Node SDK 中为Created = 'state:created'、Updated = 'state:updated'、Deleted = 'state:deleted'(sdk/packages/node/iii/src/state.ts)。
由于事件载荷同时携带old_value与new_value,处理器天然具备"比较前后差异"的能力——这正是后面条件触发与差异计算的基础。
端到端实战:Node / TypeScript 示例
下面是一个完整的响应式流程:先写入一条用户状态,再注册一个监听usersscope 的state触发器函数,最后再次set触发它。
import { iii } from './iii' import { StateEventType } from 'iii-sdk/state' // 1. 注册处理器函数:代码与普通函数无异 const onUserUpdated = iii.registerFunction( 'state::onUserUpdated', async (event) => { if (event.type === 'state' && event.event_type === StateEventType.Updated) { console.log('State changed:', event.event_type, event.key) console.log('Previous:', event.old_value) console.log('Current:', event.new_value) } return {} }, ) // 2. 注册 state 触发器:绑定函数到 users scope iii.registerTrigger({ type: 'state', function_id: onUserUpdated.id, config: { scope: 'users' }, }) // 3. 写入状态 → 引擎评估触发器 → 自动调用 onUserUpdated await iii.trigger({ function_id: 'state::set', payload: { scope: 'users', key: 'user-123', value: { name: 'Alice', email: 'alice@example.com' } }, })这段流程被仓库中的集成测试完整覆盖:在 sdk/packages/node/iii/tests/state.test.ts 的reactive state用例中,测试先state::set写入初始值,注册监听{ scope, key }的state触发器函数,再次state::set写入新值后,断言处理器确实被调用且收到的new_value等于新写入的数据。测试还展示了对称的清理流程:trigger?.unregister()与stateUpdatedFunction?.unregister()。
SDK 示例仓库中也有同样的最小写法(sdk/packages/node/iii-example/src/trigger-types.ts):
iii.registerFunction('example::on_user_updated', async (data: { event_type?: string }) => ({ processed: true, event: data.event_type, })) iii.registerTrigger({ type: 'state', function_id: 'example::on_user_updated', config: { scope: 'users' }, })Python 与 Rust 示例
同一模式在 Python 与 Rust SDK 中完全等价。Python 侧的状态类型定义(StateEventType、StateEventData)位于 sdk/packages/python/iii/src/iii/state.py:
def on_user_updated(event): print('State changed:', event['event_type'], event['key']) print('Previous:', event.get('old_value')) print('Current:', event.get('new_value')) return {} iii.register_function("state::onUserUpdated", on_user_updated) iii.register_trigger({ 'type': 'state', 'function_id': 'state::onUserUpdated', 'config': {'scope': 'users', 'key': 'profile'}, })Rust 侧使用RegisterTriggerInput配合StateTriggerConfig(sdk/packages/rust/iii/src/builtin_triggers.rs):
use iii_sdk::{RegisterFunctionMessage, RegisterTriggerInput}; use serde_json::json; iii.register_function( RegisterFunctionMessage::with_id("state::onUserUpdated".into()), |event| async move { println!("State changed: {} {}", event["event_type"], event["key"]); println!("Previous: {:?}", event.get("old_value")); println!("Current: {:?}", event.get("new_value")); Ok(json!({})) }, ); iii.register_trigger(RegisterTriggerInput { trigger_type: "state".into(), function_id: "state::onUserUpdated".into(), config: json!({ "scope": "users", "key": "profile" }), metadata: None, })?;条件触发:只处理真正相关的变更
condition_function_id允许在触发与执行之间插入一道"闸门":引擎会先把状态事件交给条件函数评估,只有返回false以外的结果时才继续调用处理器。这在"状态确实变了,但并非每次变化都值得处理"的场景中非常有用。
例如,只在邮箱字段真正变化时才发送验证邮件:
const conditionFn = iii.registerFunction( 'conditions::emailChanged', async (event) => event.event_type === 'state:updated' && event.old_value?.email !== event.new_value?.email, ) const fn = iii.registerFunction('state::onEmailChange', async (event) => { await sendVerificationEmail(event.new_value.email) return {} }) iii.registerTrigger({ type: 'state', function_id: fn.id, config: { scope: 'users', key: 'profile', condition_function_id: conditionFn.id, }, })条件的编写方式在 docs/0-16-0/how-to/use-trigger-conditions.mdx 中有系统讲解,可与本文结合阅读。
引擎视角:触发器如何被匹配与调度
理解引擎侧的调度逻辑,能帮助你更准确地预估触发行为。从源码结构看,iii 引擎的核心职责包括:维护已连接 Worker 的实时注册表、跟踪各 Worker 注册的函数与触发器、以及在触发器命中时把调用路由到提供目标函数的 Worker(参见 docs/0-16-0/understanding-iii/engine.mdx)。
state触发器类型在引擎触发器目录中注册(engine/src/trigger.rs),其配置与载荷的 JSON Schema 由JsonSchema派生自动生成(engine/src/trigger_formats.rs),因此 CLI 与工具链可以程序化地发现该触发器的配置形状。一次完整的状态触发链路为:
- 某个 Worker 调用
state::set/state::update/state::delete写入状态; - 引擎持久化到状态适配器,并得到
old_value与new_value; - 引擎把变更事件与触发器注册表中的所有
state触发器逐一匹配(scope / key 精确匹配); - 命中后(若有
condition_function_id先评估条件函数)把StateCallRequest载荷路由到绑定函数的宿主 Worker 并调用。
需要说明的是,原模式文档在 TODO 中列出的"ordering guarantees(顺序保证)"与"bursts of changes(变更突发)"两个话题尚未在文档中展开。从现有材料可以确认的边界包括:state::update本身是原子操作(一组ops要么整体生效),这为"一次写入产生一次事件"提供了基础;而若要过滤高频变更中的无关部分,最直接的手段就是condition_function_id。具体的跨事件排序语义,建议以对应版本 Worker 文档的后续更新为准。
配置状态 Worker:适配器选择
状态 Worker 本身可以配置不同的持久化与分布后端。参考同系列 Worker 文档 docs/0-11-0/workers/iii-state.mdx(该文档对state触发器的 Worker Docs 做了完整展开),内置适配器包括:
- name: iii-state config: adapter: name: kv config: store_method: file_based file_path: ./data/state_store save_interval_ms: 5000| 适配器 | 说明 | 关键配置 |
|---|---|---|
kv | 内置键值存储,支持内存与文件持久化 | store_method(in_memory/file_based)、file_path、save_interval_ms(默认5000) |
redis | 以 Redis 作为状态后端 | redis_url(支持${REDIS_URL:...}环境变量占位符) |
bridge | 通过 Bridge Client 转发状态操作到远端 III 引擎实例 | 无 |
选择持久化适配器时需注意:in_memory模式在 Worker 重启后数据会丢失,生产环境建议使用file_based或redis,以保证响应式流程所依赖的源状态可靠。
实战注意事项小结
- 函数复用:处理函数与普通函数无异,可同时被多个触发器绑定(HTTP + state),复用同一逻辑;
- 精确匹配优于模糊监听:
scope/key均为精确匹配,尽量限定监听范围,避免全量监听带来无效调用; - 善用
condition_function_id:把"是否需要执行"的判断从处理器中抽离,配合old_value/new_value做差异比较; - 清理注册:在 Worker 卸载或测试结束时调用
trigger.unregister()与函数unregister()(参考 sdk/packages/node/iii/tests/state.test.ts),避免悬挂注册; - 原子更新:需要"读-改-写"时优先使用
state::update的 ops 序列,减少中间态事件; - 事件类型三态:
state:created/state:updated/state:deleted是三种独立事件,处理器可按event_type分流,old_value为null即新建、new_value为null即删除。
延伸阅读
- 模式文档源文件:docs/patterns/reactive-state-pattern.mdx(0-16-0 版本位于 docs/0-16-0/patterns/reactive-state-pattern.mdx)
- 状态 Worker 完整文档(配置、函数、事件载荷、错误码):docs/0-11-0/workers/iii-state.mdx
- 触发器类型定义与 JSON Schema 生成:engine/src/trigger_formats.rs
- 触发器注册与调度:engine/src/trigger.rs
- Node SDK 状态类型与接口:sdk/packages/node/iii/src/state.ts
- Python SDK 状态类型:sdk/packages/python/iii/src/iii/state.py
- Rust SDK 触发器配置构建器:sdk/packages/rust/iii/src/builtin_triggers.rs
- 响应式触发集成测试:sdk/packages/node/iii/tests/state.test.ts
- 触发器条件编写指南:docs/0-16-0/how-to/use-trigger-conditions.mdx
【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考