refactor: auto flows (#7577)

* feat: add beforeRender event

* feat: refactor auto flows

* feat: add disptach event hooks

* feat: update cache for flow execution

* refactor: auto flows hook

* chore: remove applyAutoFlows

* chore: update doc

* chore: update docs for auto flow

* chore: update docs and tests

* fix: not rerender after updating step params

* chore: update docs

* fix: error fallback bug
This commit is contained in:
gchust
2025-10-13 08:42:46 +08:00
committed by GitHub
parent e87fdcd8fa
commit fa25d63b8b
64 changed files with 934 additions and 758 deletions
-4
View File
@@ -124,10 +124,6 @@ export default defineConfig({
title: 'Fork model',
link: '/examples/flow-models/fork-model',
},
// {
// title: 'cleanRun:自动流干净运行',
// link: '/examples/flow-models/render-in-fork',
// },
{
title: '拖拽(DND)',
link: '/examples/flow-models/dnd',
@@ -10,7 +10,9 @@
interface FlowDefinition<TModel extends FlowModel = FlowModel> {
key: string; // 流唯一标识
title?: string; // 可选:流显示名称
// 注意:自动执行策略:未配置 on 且未显式 manual: true 的流,视为自动执行
// 注意:自动执行策略:
// 1) 未配置 on 且未显式 manual: true 的流,视为自动执行;
// 2) 等价地,你也可以显式声明 on: 'beforeRender' 将其作为“渲染前生命周期事件”流;
manual?: boolean; // 可选:是否仅手动执行
sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数
on?: { eventName: string }; // 可选:事件触发配置
@@ -126,7 +128,8 @@ MyFlowModel.registerFlow(myFlow); // 注册流
class MyFlowDefinition implements FlowDefinition {
key = 'MyFlowDefinition';
title = '我的复杂流程';
auto = true;
// 等价于未显式声明 on 且 manual !== true 的“自动流”
on = { eventName: 'beforeRender' };
sort = 0;
steps = {
@@ -215,7 +218,7 @@ myModel.setStepParams('myFlow', 'step1', { name: '小明' });
```ts
await myModel.applyFlow('myFlow'); // 主动执行指定流
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.eventName)
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排序
await myModel.dispatchEvent('beforeRender'); // beforeRender 默认顺序执行并使用缓存(可覆盖)
```
---
@@ -18,10 +18,7 @@ interface FlowModelRendererProps {
/** 是否在设置中隐藏移除按钮 */
hideRemoveInSettings?: boolean; // 默认 false
/** 是否跳过自动应用流,默认 false */
skipApplyAutoFlows?: boolean; // 默认 false
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的运行时参数 */
/** 传递给 beforeRender 事件的运行时参数 */
inputArgs?: Record<string, any>
/** 设置菜单层级:1=仅当前模型(默认),2=包含子模型 */
@@ -43,8 +40,7 @@ interface FlowModelRendererProps {
- `modal`: 模态框形式(待实现)
- `drawer`: 抽屉形式(待实现)
- **hideRemoveInSettings**: 是否在设置中隐藏移除按钮,当设为 `true` 时,流设置菜单中不会显示删除/移除选项
- **skipApplyAutoFlows**: 是否跳过自动应用流。当设为 `true` 时,组件不会调用 `useApplyAutoFlows` hook
- **inputArgs**: 运行时参数,当 `skipApplyAutoFlows` 为 `false` 时传递给 `useApplyAutoFlows` hook
- **inputArgs**: 运行时参数,传递给 beforeRender 事件
- **settingsMenuLevel**: 设置菜单层级,控制设置菜单的显示范围
- `1`: 仅显示当前模型的设置(默认)
- `2`: 包含子模型的设置
@@ -70,10 +66,10 @@ interface FlowModelRendererProps {
hideRemoveInSettings={true}
/>
// 跳过自动应用流
// 传递 beforeRender 事件上下文
<FlowModelRenderer
model={model}
skipApplyAutoFlows={true}
inputArgs={{ customData: 'value' }}
/>
// 传递额外上下文
@@ -96,7 +92,6 @@ interface FlowModelRendererProps {
showFlowSettings={true}
flowSettingsVariant="contextMenu"
hideRemoveInSettings={false}
skipApplyAutoFlows={false}
settingsMenuLevel={2}
inputArgs={{
userId: 123,
@@ -136,17 +131,7 @@ interface FlowModelRendererProps {
/>
```
### 4. 自定义流控制
当需要手动控制流应用时,可以跳过自动流:
```tsx | pure
<FlowModelRenderer
model={flowModel}
skipApplyAutoFlows={true}
/>
```
### 5. 传递自定义上下文
### 4. 传递自定义上下文
当需要向流传递特定上下文数据时:
```tsx | pure
@@ -160,7 +145,7 @@ interface FlowModelRendererProps {
/>
```
### 6. 控制设置菜单层级
### 5. 控制设置菜单层级
当需要控制设置菜单显示范围时,可以通过 `settingsMenuLevel` 参数配置:
```tsx | pure
@@ -75,14 +75,14 @@
- **applyFlow(flowKey: string, inputArgs?: Record<string, any>): Promise\<any\>**
执行指定流。
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>): void**
触发事件,自动匹配并执行相关流。
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>, options?: { debounce?: boolean; sequential?: boolean; useCache?: boolean }): Promise<any[]>**
触发事件并执行匹配的流;支持顺序或并行执行。
- `sequential`: 是否顺序执行(默认并行);beforeRender 的默认值为顺序执行(可覆盖)。
- `useCache`: 是否启用事件层面的缓存(默认 `false`);beforeRender 的默认值为启用缓存(可覆盖)。
- `debounce`: 是否启用防抖(默认 `false`)。
- **applyAutoFlows(inputArgs?: Record<string, any>): Promise\<any[]\>**
执行所有自动流。
- **applySubModelsAutoFlows(subKey: string, extra?: Record\<string, any\>): Promise\<void\>**
执行指定子模型的自动流。
- **applySubModelsBeforeRenderFlows(subKey: string, inputArgs?: Record\<string, any\>): Promise\<void\>**
对指定子模型派发 `beforeRender` 事件(内部顺序执行并使用缓存)。
- **getFlow(key: string): FlowDefinition \| undefined**
获取指定 key 的流配置。
@@ -90,23 +90,22 @@
- **getFlows(): Map\<string, FlowDefinition\>**
获取所有已配置流(含继承)。
- **getAutoFlows(): FlowDefinition[]**
获取所有自动应用流程定义并按 sort 排序。
- **getEventFlows(eventName: string): FlowDefinition[]**
按事件名获取流程集合并按 sort 排序。
- 特殊事件 `beforeRender`: 兼容包含“未声明 on 且 manual !== true”的流程。
---
### 自动流执行生命周期钩子
### 事件分发钩子
FlowModel 提供了三个全局自动流执行生命周期钩子,子类可以重写这些方法来实现自定义逻辑:
- **async onDispatchEventStart(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>)**
事件执行前调用;beforeRender 可通过抛出 `FlowExitException` 终止执行。
- **async onBeforeAutoFlows(inputArgs?: Record<string, any>): Promise\<void\>**
在所有自动流执行前调用。可以通过抛出 `FlowExitException` 来终止后续流程执行。
- **async onDispatchEventEnd(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, results?: any[])**
事件执行后调用;`results` 为按 sort 顺序收集到的步骤结果数组。
- **async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>): Promise\<void\>**
在所有自动流执行后调用。可以访问所有自动流的执行结果。
- **async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>): Promise\<void\>**
在自动流执行或生命周期钩子出错时调用。可进行自定义错误处理。
- **async onDispatchEventError(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, error?: Error)**
事件执行或生命周期钩子出错时调用。
---
@@ -207,7 +206,7 @@ await model.save();
// 执行流
await model.applyFlow('default');
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
await model.dispatchEvent('event');
```
@@ -241,3 +240,5 @@ interface DefaultStructure {
subModels?: Record<string, FlowModel | FlowModel[]>;
}
```
- **invalidateFlowCache(eventName?: string, deep?: boolean): void**
失效当前模型指定事件的流程缓存;未指定 `eventName` 时失效当前模型的全部事件缓存。常用:`invalidateFlowCache('beforeRender')`。
@@ -92,7 +92,8 @@ model.mapSubModels('tabs', (tab) => {
### 作为属性值使用
```tsx | pure
await model.applySubModelsAutoFlows(ctx);
// 先让子列模型完成 beforeRender 事件流(如列的可见性、标题、渲染器等)
await model.applySubModelsBeforeRenderFlows('columns');
const columns = model.mapSubModels('columns', (column) => column.getProps());
<Table columns={columns} />
```
@@ -168,7 +168,7 @@ const fork2 = model.createFork('key2', {});
### 1. 生命周期逻辑集中且清晰
FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onBeforeAutoFlows`、`onAfterAutoFlows` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。
FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onDispatchEventStart/End/Error` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。对于渲染前的自动执行逻辑,统一通过 `onDispatchEvent*` 钩子并判断 `eventName === 'beforeRender'`。
### 2. 支持继承与复用
@@ -205,7 +205,7 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增
| 用途 | 构建 UI 组件 | 构建数据驱动、流化、结构化的“模型树” |
| 数据结构 | 组件树 | 模型树(支持父子模型、多实例 Fork) |
| 子组件 | 使用 JSX 嵌套组件 | 使用 `setSubModel`/`addSubModel` 明确设置子模型 |
| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发 Flow、处理自动流 |
| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发/处理 Flow |
| 持久化 | 无内建机制 | 支持持久化(如 `model.save()`) |
| 支持 Fork(多次渲染) | 否(需手动复用) | 是(`createFork` 多实例化) |
| 引擎控制 | 无 | 是,受 `FlowEngine` 管理、注册和加载 |
@@ -216,8 +216,8 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增
| ------ | --------------------------------- | -------------------------------------------- |
| 初始化 | `constructor`、`componentDidMount` | `onInit`、`onMount` |
| 卸载 | `componentWillUnmount` | `onUnmount` |
| 响应输入 | `componentDidUpdate` | `onBeforeAutoFlows`、`onAfterAutoFlows` |
| 错误处理 | `componentDidCatch` | `onAutoFlowsError` |
| 响应输入 | `componentDidUpdate` | `onDispatchEventStart/End`(配合 beforeRender 事件) |
| 错误处理 | `componentDidCatch` | `onDispatchEventError` |
### 🧱 构建结构对比
@@ -250,8 +250,7 @@ class HelloModel extends FlowModel {
| 功能 | 说明 |
| -------------------------------- | ---------------------- |
| `applyAutoFlows` | 自动触发已注册的流 |
| `registerFlow` / `dispatchEvent` | 支持流引擎事件处理 |
| `registerFlow` / `dispatchEvent` | 注册/分发事件流(含 beforeRender) |
| `setSubModel` / `addSubModel` | 显式控制子模型的创建与绑定 |
| `createFork` | 支持一个模型逻辑被复用渲染多次(如表格每行) |
| `loadModel` / `save()` | 模型可持久化,与后端打通 |
@@ -289,9 +288,9 @@ class HelloModel extends FlowModel {
* `onInit(options)`
* `onMount()`
* `onUnmount()`
* `onBeforeAutoFlows(inputArgs)`
* `onAfterAutoFlows(results, inputArgs)`
* `onAutoFlowsError(error, inputArgs)`
* `onDispatchEventStart(eventName, options?, inputArgs?)`
* `onDispatchEventEnd(eventName, options?, inputArgs?, results?)`
* `onDispatchEventError(eventName, options?, inputArgs?, error?)`
### 属性和参数管理
@@ -311,7 +310,6 @@ class HelloModel extends FlowModel {
* `registerFlow`
* `dispatchEvent`
* `applyAutoFlows`
### 事件(emitter.on)
@@ -10,7 +10,6 @@
interface FlowDefinition<TModel extends FlowModel = FlowModel> {
key: string; // 流唯一标识
title?: string; // 可选:流显示名称
auto?: boolean; // 可选:是否自动运行
sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数
on?: { eventName: string }; // 可选:事件触发配置
steps: Record<string, StepDefinition<TModel>>; // 流步骤定义
@@ -120,7 +119,8 @@ MyFlowModel.registerFlow(myFlow); // 注册流
class MyFlowDefinition implements FlowDefinition {
key = 'MyFlowDefinition';
title = '我的复杂流程';
auto = true;
// 推荐显式声明 beforeRender 生命周期事件(等价于未声明 on 且 manual !== true 的流)
on = { eventName: 'beforeRender' };
sort = 0;
steps = {
@@ -209,7 +209,7 @@ myModel.setStepParams('myFlow', 'step1', { name: '小明' });
```ts
await myModel.applyFlow('myFlow'); // 主动执行指定流
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.eventName)
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排序
await myModel.dispatchEvent('beforeRender'); // beforeRender 默认顺序执行并使用缓存(可覆盖)
```
---
@@ -222,8 +222,7 @@ await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排
| ----------- | -------------------------------- | ---------------------------------- |
| `key` | `string` | 流唯一标识,必须配置 |
| `title` | `string` | (可选)流显示名称 |
| `on` | `{ eventName: string }` | (可选)事件触发配置 |
| `auto` | `boolean` | (可选)是否在 `applyAutoFlows()` 中自动执行流 |
| `on` | `{ eventName: string }` | (可选)事件触发配置(可用 `beforeRender` 作为生命周期事件) |
| `sort` | `number` | (可选)流执行排序,数字越小越先执行,默认为 0,可为负数 |
| `steps` | `Record<string, StepDefinition>` | 步骤集合,键为步骤名,值为步骤定义 |
@@ -18,10 +18,7 @@ interface FlowModelRendererProps {
/** 是否在设置中隐藏移除按钮 */
hideRemoveInSettings?: boolean; // 默认 false
/** 是否跳过自动应用流,默认 false */
skipApplyAutoFlows?: boolean; // 默认 false
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的运行时参数 */
/** 传递给 beforeRender 事件的运行时参数 */
inputArgs?: Record<string, any>
/** 设置菜单层级:1=仅当前模型(默认),2=包含子模型 */
@@ -43,8 +40,7 @@ interface FlowModelRendererProps {
- `modal`: 模态框形式(待实现)
- `drawer`: 抽屉形式(待实现)
- **hideRemoveInSettings**: 是否在设置中隐藏移除按钮,当设为 `true` 时,流设置菜单中不会显示删除/移除选项
- **skipApplyAutoFlows**: 是否跳过自动应用流。当设为 `true` 时,组件不会调用 `useApplyAutoFlows` hook
- **inputArgs**: 运行时参数,当 `skipApplyAutoFlows` 为 `false` 时传递给 `useApplyAutoFlows` hook
- **inputArgs**: 运行时参数,传递给 beforeRender 事件
- **settingsMenuLevel**: 设置菜单层级,控制设置菜单的显示范围
- `1`: 仅显示当前模型的设置(默认)
- `2`: 包含子模型的设置
@@ -70,10 +66,10 @@ interface FlowModelRendererProps {
hideRemoveInSettings={true}
/>
// 跳过自动应用流
// 传递 beforeRender 事件上下文
<FlowModelRenderer
model={model}
skipApplyAutoFlows={true}
inputArgs={{ customData: 'value' }}
/>
// 传递额外上下文
@@ -96,9 +92,8 @@ interface FlowModelRendererProps {
showFlowSettings={true}
flowSettingsVariant="contextMenu"
hideRemoveInSettings={false}
skipApplyAutoFlows={false}
settingsMenuLevel={2}
inputArgs={{
inputArgs={{
userId: 123,
permissions: ['read', 'write']
}}
@@ -136,17 +131,7 @@ interface FlowModelRendererProps {
/>
```
### 4. 自定义流控制
当需要手动控制流应用时,可以跳过自动流:
```tsx | pure
<FlowModelRenderer
model={flowModel}
skipApplyAutoFlows={true}
/>
```
### 5. 传递自定义上下文
### 4. 传递自定义上下文
当需要向流传递特定上下文数据时:
```tsx | pure
@@ -160,7 +145,7 @@ interface FlowModelRendererProps {
/>
```
### 6. 控制设置菜单层级
### 5. 控制设置菜单层级
当需要控制设置菜单显示范围时,可以通过 `settingsMenuLevel` 参数配置:
```tsx | pure
@@ -75,17 +75,14 @@
- **applyFlow(flowKey: string, inputArgs?: Record<string, any>): Promise\<any\>**
执行指定流。
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>, options?: { debounce: boolean }): Promise\<void\>**
触发事件,自动匹配并执行相关流。支持防抖功能,防止快速连续触发。
- `eventName`: 要触发的事件名称
- `inputArgs`: 可选的事件参数
- `options.debounce`: 是否启用防抖功能,默认为 `false`。当设为 `true` 时,100ms 内的重复调用会被防抖处理
- **dispatchEvent(eventName: string, inputArgs?: Record<string, any>, options?: { debounce?: boolean; sequential?: boolean; useCache?: boolean }): Promise\<any[]\>**
触发事件并执行对应的流程集合。
- `sequential`: 是否顺序执行(默认并行);beforeRender 的默认值为顺序执行(可覆盖)
- `useCache`: 是否启用事件级缓存(默认 false);beforeRender 的默认值为启用缓存(可覆盖)
- `debounce`: 是否启用防抖(默认 false)
- **applyAutoFlows(inputArgs?: Record<string, any>): Promise\<any[]\>**
执行所有自动流。
- **applySubModelsAutoFlows(subKey: string, extra?: Record\<string, any\>): Promise\<void\>**
执行指定子模型的自动流。
- **applySubModelsBeforeRenderFlows(subKey: string, inputArgs?: Record\<string, any\>): Promise\<void\>**
对指定子模型派发 `beforeRender` 事件(内部顺序执行并使用缓存)。
- **getFlow(key: string): FlowDefinition \| undefined**
获取指定 key 的流配置。
@@ -93,23 +90,17 @@
- **getFlows(): Map\<string, FlowDefinition\>**
获取所有已配置流(含继承)。
- **getAutoFlows(): FlowDefinition[]**
获取所有自动应用流程定义并按 sort 排序。
- **getEventFlows(eventName: string): FlowDefinition[]**
按事件名获取流程集合并按 sort 排序。
- 特殊事件 `beforeRender`: 兼容包含“未声明 on 且 manual !== true”的流程。
---
### 自动流执行生命周期钩子
### 事件分发钩子
FlowModel 提供了三个全局自动流执行生命周期钩子,子类可以重写这些方法来实现自定义逻辑:
- **async onBeforeAutoFlows(inputArgs?: Record<string, any>): Promise\<void\>**
在所有自动流执行前调用。可以通过抛出 `FlowExitException` 来终止后续流程执行。
- **async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>): Promise\<void\>**
在所有自动流执行后调用。可以访问所有自动流的执行结果。
- **async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>): Promise\<void\>**
在自动流执行或生命周期钩子出错时调用。可进行自定义错误处理。
- **async onDispatchEventStart(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>)**
- **async onDispatchEventEnd(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, results?: any[])**
- **async onDispatchEventError(eventName: string, options?: DispatchEventOptions, inputArgs?: Record<string, any>, error?: Error)**
---
@@ -205,7 +196,7 @@ await model.save();
// 执行流
await model.applyFlow('default');
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
// 触发事件(立即执行)
await model.dispatchEvent('click');
@@ -92,7 +92,8 @@ model.mapSubModels('tabs', (tab) => {
### 作为属性值使用
```tsx | pure
await model.applySubModelsAutoFlows(ctx);
// 先让子列模型完成 beforeRender 事件流(如列的可见性、标题、渲染器等)
await model.applySubModelsBeforeRenderFlows('columns');
const columns = model.mapSubModels('columns', (column) => column.getProps());
<Table columns={columns} />
```
@@ -103,7 +103,7 @@ class PluginDynamicDefaultParams extends Plugin {
props: { name: '示例产品', category: 'electronics', price: 0 },
});
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
this.router.add('root', {
path: '/',
@@ -110,7 +110,7 @@ class PluginDynamicUiSchema extends Plugin {
props: { formType: 'user', advancedMode: false },
});
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
this.router.add('root', {
path: '/',
@@ -41,7 +41,7 @@ export class TableModel extends FlowModel<S> {
},
},
});
model.applyAutoFlows();
model.dispatchEvent('beforeRender');
},
items: this.collection.mapFields((field) => {
return {
@@ -128,7 +128,7 @@ TableModel.registerFlow({
resource.setAPIClient(api);
ctx.model.resource = resource;
await resource.refresh();
await ctx.model.applySubModelsAutoFlows('columns');
await ctx.model.applySubModelsBeforeRenderFlows('columns');
},
},
},
+8 -10
View File
@@ -168,7 +168,7 @@ const fork2 = model.createFork('key2', {});
### 1. 生命周期逻辑集中且清晰
FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onBeforeAutoFlows`、`onAfterAutoFlows` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。
FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onDispatchEventStart/End/Error` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。对于渲染前的自动执行逻辑,统一通过 `onDispatchEvent*` 钩子并判断 `eventName === 'beforeRender'`。
### 2. 支持继承与复用
@@ -205,7 +205,7 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增
| 用途 | 构建 UI 组件 | 构建数据驱动、流化、结构化的“模型树” |
| 数据结构 | 组件树 | 模型树(支持父子模型、多实例 Fork) |
| 子组件 | 使用 JSX 嵌套组件 | 使用 `setSubModel`/`addSubModel` 明确设置子模型 |
| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发 Flow、处理自动流 |
| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发/处理 Flow |
| 持久化 | 无内建机制 | 支持持久化(如 `model.save()`) |
| 支持 Fork(多次渲染) | 否(需手动复用) | 是(`createFork` 多实例化) |
| 引擎控制 | 无 | 是,受 `FlowEngine` 管理、注册和加载 |
@@ -216,8 +216,8 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增
| ------ | --------------------------------- | -------------------------------------------- |
| 初始化 | `constructor`、`componentDidMount` | `onInit`、`onMount` |
| 卸载 | `componentWillUnmount` | `onUnmount` |
| 响应输入 | `componentDidUpdate` | `onBeforeAutoFlows`、`onAfterAutoFlows` |
| 错误处理 | `componentDidCatch` | `onAutoFlowsError` |
| 响应输入 | `componentDidUpdate` | `onDispatchEventStart/End`(配合 beforeRender 事件) |
| 错误处理 | `componentDidCatch` | `onDispatchEventError` |
### 🧱 构建结构对比
@@ -250,8 +250,7 @@ class HelloModel extends FlowModel {
| 功能 | 说明 |
| -------------------------------- | ---------------------- |
| `applyAutoFlows` | 自动触发已注册的流 |
| `registerFlow` / `dispatchEvent` | 支持流引擎事件处理 |
| `registerFlow` / `dispatchEvent` | 注册/分发事件流(含 beforeRender) |
| `setSubModel` / `addSubModel` | 显式控制子模型的创建与绑定 |
| `createFork` | 支持一个模型逻辑被复用渲染多次(如表格每行) |
| `loadModel` / `save()` | 模型可持久化,与后端打通 |
@@ -289,9 +288,9 @@ class HelloModel extends FlowModel {
* `onInit(options)`
* `onMount()`
* `onUnmount()`
* `onBeforeAutoFlows(inputArgs)`
* `onAfterAutoFlows(results, inputArgs)`
* `onAutoFlowsError(error, inputArgs)`
* `onDispatchEventStart(eventName, options?, inputArgs?)`
* `onDispatchEventEnd(eventName, options?, inputArgs?, results?)`
* `onDispatchEventError(eventName, options?, inputArgs?, error?)`
### 属性和参数管理
@@ -311,7 +310,6 @@ class HelloModel extends FlowModel {
* `registerFlow`
* `dispatchEvent`
* `applyAutoFlows`
### 事件(emitter.on)
@@ -14,9 +14,11 @@ class AsyncValueModel extends FlowModel {
},
});
}
async onBeforeAutoFlows() {
this.asyncValue = await this.context.asyncValue;
console.log('Async value prepared:', this.asyncValue);
async onDispatchEventStart(eventName: string) {
if (eventName === 'beforeRender') {
this.asyncValue = await this.context.asyncValue;
console.log('Async value prepared:', this.asyncValue);
}
}
render() {
return (
@@ -17,9 +17,11 @@ class AsyncValueModel extends FlowModel {
},
});
}
async onBeforeAutoFlows() {
await Promise.all([this.context.concurrent, this.context.concurrent, this.context.concurrent]);
this.concurrent = await this.context.concurrent;
async onDispatchEventStart(eventName: string) {
if (eventName === 'beforeRender') {
await Promise.all([this.context.concurrent, this.context.concurrent, this.context.concurrent]);
this.concurrent = await this.context.concurrent;
}
}
render() {
return (
@@ -2,16 +2,20 @@
## 在 Model 内部使用
建议在 `onBeforeAutoFlows` 或 `onAfterAutoFlows` 方法中处理异步属性,系统会自动维护 loading 状态。例如:
建议在 `onDispatchEventStart/End` 钩子中处理渲染前(beforeRender)相关的异步属性,系统会自动维护 loading 状态。例如:
```ts
class MyModel {
async onBeforeAutoFlows() {
this.asyncProperty1 = await this.context.asyncProperty2;
async onDispatchEventStart(eventName: string) {
if (eventName === 'beforeRender') {
this.asyncProperty1 = await this.context.asyncProperty2;
}
}
async onAfterAutoFlows() {
this.asyncProperty1 = await this.context.asyncProperty2;
async onDispatchEventEnd(eventName: string) {
if (eventName === 'beforeRender') {
this.asyncProperty1 = await this.context.asyncProperty2;
}
}
}
```
@@ -4,7 +4,7 @@
## 以下流执行时会创建 FlowRuntimeContext
- `model.applyAutoFlows(inputArgs)`
- `model.dispatchEvent('beforeRender', inputArgs)`
自动执行所有属性流,`inputArgs` 为流输入参数。
- `model.dispatchEvent(eventName, inputArgs)`
@@ -1,6 +1,6 @@
# 错误回退:CreateForm 自动流异常
# 错误回退:CreateForm beforeRender事件流异常
当自动流(useApplyAutoFlows)执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。
当beforeRender事件流执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。
<code src="./create-form-auto-flows-error.tsx"></code>
@@ -3,15 +3,7 @@
* title: 错误回退 - CreateForm 自动流异常
*/
import {
Application,
CreateFormModel,
FilterManager,
FormFieldModel,
FormGridModel,
FormItemModel,
Plugin,
} from '@nocobase/client';
import { Application, CreateFormModel, FilterManager, FormGridModel, FormItemModel, Plugin } from '@nocobase/client';
import { FlowEngineProvider, FlowModelRenderer } from '@nocobase/flow-engine';
import { APIClient } from '@nocobase/sdk';
import { Card } from 'antd';
@@ -45,7 +37,6 @@ class DemoPlugin extends Plugin {
CreateFormModel,
FormGridModel,
FormItemModel,
FormFieldModel,
});
this.form = this.flowEngine.createModel({
@@ -97,7 +88,7 @@ class DemoPlugin extends Plugin {
element: (
<FlowEngineProvider engine={this.flowEngine}>
<div style={{ padding: 16 }}>
<Card title="CreateForm 自动流异常演示" style={{ marginTop: 12 }}>
<Card title="CreateForm beforeRender事件流异常演示" style={{ marginTop: 12 }}>
<FlowModelRenderer model={this.form} showFlowSettings showErrorFallback />
</Card>
</div>
@@ -3,7 +3,7 @@
本页汇总并演示三类常见的错误回退场景,便于一次性对比查看:
- CreateForm 字段渲染异常
- CreateForm 自动流(useApplyAutoFlows)异常
- CreateForm beforeRender事件流
- 表格列(RenderFunction)单元格渲染异常
## CreateForm 字段异常
@@ -12,7 +12,7 @@
<code src="./create-form-field-error.tsx"></code>
## CreateForm 自动流异常
## CreateForm beforeRender事件流异常
当自动流(useApplyAutoFlows)执行过程中发生异常时,错误将被提升为渲染期错误并由外层错误边界捕获。
@@ -15,12 +15,16 @@ class HelloModel extends FlowModel {
console.log('onUnmount');
}
async onBeforeAutoFlows() {
console.log('onBeforeAutoFlows');
async onDispatchEventStart(eventName: string) {
if (eventName === 'beforeRender') {
console.log('onDispatchEventStart(beforeRender)');
}
}
async onAfterAutoFlows() {
console.log('onAfterAutoFlows');
async onDispatchEventEnd(eventName: string) {
if (eventName === 'beforeRender') {
console.log('onDispatchEventEnd(beforeRender)');
}
}
render() {
@@ -1,8 +0,0 @@
---
title: 模型 cleanRun:每次自动流干净运行
order: 99
---
本示例演示如何通过模型选项 `cleanRun` 实现“每次自动流都在干净基线”运行,避免状态累积污染原始模型。
<code src="./index.tsx"></code>
@@ -1,112 +0,0 @@
/**
* defaultShowCode: true
* title: 模型 cleanRun:每次自动流干净运行
*/
import React from 'react';
import { Application, Plugin } from '@nocobase/client';
import { Card, Space } from 'antd';
import { FlowModel, FlowModelRenderer, defineFlow, escapeT } from '@nocobase/flow-engine';
class DemoModel extends FlowModel {
static {
// 配置流:修改卡片标题(演示可配置项)
this.registerFlow(
defineFlow({
key: 'demoSettings',
title: escapeT('Demo settings'),
manual: true,
steps: {
title: {
title: escapeT('Title'),
uiSchema: {
title: {
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-component-props': { placeholder: escapeT('Please input title') },
},
},
defaultParams: (ctx) => ({ title: (ctx.model.getProps() as any).title ?? '' }),
handler: (ctx, params) => {
ctx.model.setProps({ title: params.title });
},
},
},
}),
);
// 自动流:每次运行把 count + 1,并记录 runId
this.registerFlow(
defineFlow({
key: 'autoCounter',
title: 'Auto counter',
steps: {
bumpCount: {
handler: (ctx) => {
const props = ctx.model.getProps() as any;
const prev = Number(props.count) || 0;
ctx.model.setProps({ count: prev + 1, lastRunId: ctx.runId });
},
},
},
}),
);
}
render() {
const { count = 0, lastRunId, title } = this.getProps() as any;
return (
<Card size="small" style={{ width: 360 }}>
<div>
title: <b>{title || 'Demo'}</b>
</div>
<div>
count: <b>{count}</b>
</div>
<div style={{ wordBreak: 'break-all' }}>
lastRunId: <code>{String(lastRunId || '')}</code>
</div>
</Card>
);
}
}
class PluginDemo extends Plugin {
async load() {
this.flowEngine.registerModels({ DemoModel });
// 强制启用 FlowSettings(本示例已不依赖设置面板,保留无害)
this.flowEngine.flowSettings.forceEnable();
const model1 = this.flowEngine.createModel({ use: 'DemoModel', cleanRun: false });
const model2 = this.flowEngine.createModel({ use: 'DemoModel', cleanRun: true });
const View = () => {
return (
<Space direction="vertical" size={16}>
<Space align="start">
<Space direction="vertical">
<div>cleanRun: false(状态累加)</div>
<FlowModelRenderer model={model1} showFlowSettings />
</Space>
<Space direction="vertical">
<div>cleanRun: true(每次干净基线)</div>
<FlowModelRenderer model={model2} showFlowSettings />
</Space>
</Space>
</Space>
);
};
this.router.add('root', {
path: '/',
element: <View />,
});
}
}
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginDemo],
});
export default app.getRootComponent();
@@ -168,7 +168,7 @@ const fork2 = model.createFork('key2', {});
### 1. 生命周期逻辑集中且清晰
FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onBeforeAutoFlows`、`onAfterAutoFlows` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。
FlowModel 拥有多个生命周期钩子(如 `onInit`、`onMount`、`onUnmount`、`onDispatchEventStart/End/Error` 等),这些逻辑通过类方法更清晰地组织,避免了函数式中多个 Hook 间的复杂依赖。对于渲染前的自动执行逻辑,统一通过 `onDispatchEvent*` 钩子并判断 `eventName === 'beforeRender'`。
### 2. 支持继承与复用
@@ -205,7 +205,7 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增
| 用途 | 构建 UI 组件 | 构建数据驱动、流化、结构化的“模型树” |
| 数据结构 | 组件树 | 模型树(支持父子模型、多实例 Fork) |
| 子组件 | 使用 JSX 嵌套组件 | 使用 `setSubModel`/`addSubModel` 明确设置子模型 |
| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发 Flow、处理自动流 |
| 动态行为 | 事件绑定、状态更新驱动 UI | 注册/派发/处理 Flow |
| 持久化 | 无内建机制 | 支持持久化(如 `model.save()`) |
| 支持 Fork(多次渲染) | 否(需手动复用) | 是(`createFork` 多实例化) |
| 引擎控制 | 无 | 是,受 `FlowEngine` 管理、注册和加载 |
@@ -216,8 +216,8 @@ FlowModel 并不会改变组件的实现方式。它只是为 ReactComponent 增
| ------ | --------------------------------- | -------------------------------------------- |
| 初始化 | `constructor`、`componentDidMount` | `onInit`、`onMount` |
| 卸载 | `componentWillUnmount` | `onUnmount` |
| 响应输入 | `componentDidUpdate` | `onBeforeAutoFlows`、`onAfterAutoFlows` |
| 错误处理 | `componentDidCatch` | `onAutoFlowsError` |
| 响应输入 | `componentDidUpdate` | `onDispatchEventStart/End`(配合 beforeRender 事件) |
| 错误处理 | `componentDidCatch` | `onDispatchEventError` |
### 🧱 构建结构对比
@@ -250,8 +250,7 @@ class HelloModel extends FlowModel {
| 功能 | 说明 |
| -------------------------------- | ---------------------- |
| `applyAutoFlows` | 自动触发已注册的流 |
| `registerFlow` / `dispatchEvent` | 支持流引擎事件处理 |
| `registerFlow` / `dispatchEvent` | 注册/分发事件流(含 beforeRender) |
| `setSubModel` / `addSubModel` | 显式控制子模型的创建与绑定 |
| `createFork` | 支持一个模型逻辑被复用渲染多次(如表格每行) |
| `loadModel` / `save()` | 模型可持久化,与后端打通 |
@@ -289,9 +288,9 @@ class HelloModel extends FlowModel {
* `onInit(options)`
* `onMount()`
* `onUnmount()`
* `onBeforeAutoFlows(inputArgs)`
* `onAfterAutoFlows(results, inputArgs)`
* `onAutoFlowsError(error, inputArgs)`
* `onDispatchEventStart(eventName, options?, inputArgs?)`
* `onDispatchEventEnd(eventName, options?, inputArgs?, results?)`
* `onDispatchEventError(eventName, options?, inputArgs?, error?)`
### 属性和参数管理
@@ -311,7 +310,6 @@ class HelloModel extends FlowModel {
* `registerFlow`
* `dispatchEvent`
* `applyAutoFlows`
### 事件(emitter.on)
@@ -74,7 +74,7 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
};
const pageModel = {
context: pageModelContext,
invalidateAutoFlowCache: vi.fn(),
invalidateFlowCache: vi.fn(),
_rerunLastAutoRun: vi.fn(),
} as any;
@@ -137,7 +137,7 @@ describe('openView action - route mode defineProperties/defineMethods', () => {
};
const pageModel = {
context: pageModelContext,
invalidateAutoFlowCache: vi.fn(),
invalidateFlowCache: vi.fn(),
_rerunLastAutoRun: vi.fn(),
} as any;
@@ -702,7 +702,7 @@ export const openView = defineAction({
pageModel.context.defineMethod(key, method);
});
pageModel.invalidateAutoFlowCache(true);
pageModel.invalidateFlowCache('beforeRender', true);
pageModel['_rerunLastAutoRun'](); // TODO: 临时做法,等上下文重构完成后去掉
}}
/>
@@ -722,7 +722,7 @@ export const openView = defineAction({
const nav = ctx.inputArgs?.navigation || ctx.view?.navigation;
if (pageModelUid) {
const pageModel = pageModelRef || ctx.model.flowEngine.getModel(pageModelUid);
pageModel?.invalidateAutoFlowCache(true);
pageModel?.invalidateFlowCache('beforeRender', true);
}
if (navigation !== false) {
if (nav?.back) {
@@ -101,6 +101,6 @@ async function rebuildFieldSubModel(ctx, model, binding: BindingOptions) {
},
});
await subModel.applyAutoFlows();
await subModel.dispatchEvent('beforeRender');
await model.save(); // 持久化
}
@@ -76,7 +76,7 @@ export const titleField = defineAction({
},
});
await model.save();
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
// 持久化
await ctx.model.save();
}
@@ -109,7 +109,7 @@ export const titleField = defineAction({
},
});
// await model.save();
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
}
},
});
@@ -41,7 +41,8 @@ interface Props {
function createTempFieldClass(Base: any) {
return class Temp extends Base {
async onBeforeAutoFlows() {
async onDispatchEventStart(eventName: string) {
if (eventName !== 'beforeRender') return;
const initParams = this.getStepParams?.('fieldSettings', 'init') || {};
const collectionFieldKey =
initParams?.dataSourceKey &&
@@ -56,7 +57,8 @@ function createTempFieldClass(Base: any) {
get: () => collectionFieldFromManager || fallbackCollectionField,
});
}
async onAfterAutoFlows() {
async onDispatchEventEnd(eventName: string) {
if (eventName !== 'beforeRender') return;
const originalProps = this._originalModel?.props || {};
const collectionField = this.context?.collectionField;
const inferMultipleFromCollectionField = () => {
@@ -116,7 +116,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({ fieldUid, value, onChan
updateAssociation: false,
multiple,
});
fm?.applyAutoFlows?.();
fm?.dispatchEvent?.('beforeRender', undefined, { sequential: true, useCache: true });
// 为本地枚举型字段补全可选项(仅在未显式传入 options 时处理)
ensureOptionsFromUiSchemaEnumIfAbsent(fm, cf);
if (!fm?.props?.fieldNames && cf?.targetCollection) {
@@ -101,7 +101,7 @@ describe('DefaultValue component', () => {
// 每个用例重置表单实例,避免跨用例的值污染(例如上个用例输入的 "hello")
form = createForm();
// 为多选关联选择组件的测试场景,覆盖/简化自动流程,只保留必要的属性赋值
// 为多选关联选择组件的测试场景,覆盖/简化 beforeRender 事件流,仅保留必要的属性赋值
RecordSelectFieldModel.registerFlow({ key: 'eventSettings', manual: true, steps: {} });
RecordSelectFieldModel.registerFlow({
key: 'selectSettings',
@@ -193,7 +193,7 @@ describe('DefaultValue component', () => {
},
},
});
// 指定为对多关系,DefaultValue 内部应选择 RecordSelectFieldModel,并在 onAfterAutoFlows 推断 multiple
// 指定为对多关系,DefaultValue 内部应选择 RecordSelectFieldModel,并在 onDispatchEventEnd 推断 multiple
host.context.defineProperty('collectionField', {
value: {
interface: 'm2m',
@@ -24,14 +24,14 @@ import {
} from '@nocobase/flow-engine';
import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
// Minimal auto-flow model that executes preview code
// Minimal beforeRender event model that executes preview code
class DummyJsAutoModel extends FlowModel {
render() {
return <span data-testid={`dummy-${this.uid}`} ref={this.context.ref as any} />;
}
}
describe('useCodeRunner (auto flow)', () => {
describe('useCodeRunner (beforeRender)', () => {
it('logs success and captures console output', async () => {
const engine = new FlowEngine();
engine.registerModels({ DummyJsAutoModel });
@@ -82,7 +82,7 @@ describe('useCodeRunner (auto flow)', () => {
expect(result.current.running).toBe(false);
});
it('previews and updates DOM for JSEditable (auto flow)', async () => {
it('previews and updates DOM for JSEditable (beforeRender)', async () => {
const engine = new FlowEngine();
engine.registerModels({ JSEditableFieldModel });
const field = engine.createModel<JSEditableFieldModel>({ use: 'JSEditableFieldModel', uid: 'f1' });
@@ -106,7 +106,11 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
if (isManual) {
await m.applyFlow('jsSettings', { preview: { code, version } });
} else {
await m.applyAutoFlows({ preview: { code, version } }, false);
await m.dispatchEvent(
'beforeRender',
{ preview: { code, version } },
{ sequential: true, useCache: false },
);
}
};
@@ -297,7 +297,7 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
resource.setDataSourceKey(params.dataSourceKey);
resource.setResourceName(params.associationName || params.collectionName);
resource.on('refresh', () => {
this.invalidateAutoFlowCache();
this.invalidateFlowCache('beforeRender');
});
return resource;
},
@@ -124,7 +124,7 @@ export class RootPageTabModel extends BasePageTabModel {
async destroy() {
this.observerDispose();
this.invalidateAutoFlowCache(true);
this.invalidateFlowCache('beforeRender', true);
this.flowEngine.removeModel(this.uid);
await this.context.api.request({
method: 'post',
@@ -172,7 +172,7 @@ export class AssignFormItemModel extends FormItemModel {
updateAssociation: false,
multiple: multi,
});
fm?.applyAutoFlows?.();
fm?.dispatchEvent?.('beforeRender', undefined, { sequential: true, useCache: true });
// 为本地枚举型字段补全可选项(仅在未显式传入 options 时处理)
ensureOptionsFromUiSchemaEnumIfAbsent(fm as any, cfForMultiple as any);
if (!fm?.props?.fieldNames && cfForMultiple?.targetCollection) {
@@ -175,7 +175,7 @@ DetailsBlockModel.registerFlow({
throw new Error('Resource is not initialized');
}
// 1. 先初始化字段网格,确保所有字段都创建完成
await ctx.model.applySubModelsAutoFlows('grid');
await ctx.model.applySubModelsBeforeRenderFlows('grid');
},
},
layout: {
@@ -234,7 +234,7 @@ QuickEditFormModel.registerFlow({
get: () => collectionField,
});
ctx.model.addAppends(fieldPath);
await fieldModel.applyAutoFlows();
await fieldModel.dispatchEvent('beforeRender');
}
if (ctx.inputArgs.filterByTk || ctx.inputArgs.record) {
resource.setFilterByTk(ctx.inputArgs.filterByTk);
@@ -88,7 +88,7 @@ const AddActionToolbarComponent = ({ model }) => {
export class TableActionsColumnModel extends TableCustomColumnModel {
async afterAddAsSubModel() {
await this.applyAutoFlows();
await this.dispatchEvent('beforeRender');
}
getColumnProps() {
@@ -587,7 +587,7 @@ TableBlockModel.registerFlow({
ctx.model.mapSubModels('columns', async (column) => {
if (column.hidden) return;
try {
await column.applyAutoFlows();
await column.dispatchEvent('beforeRender');
} catch (err) {
column['__autoFlowError'] = err;
// 列级错误不再向上抛,避免拖垮整表;在单元格层用 ErrorBoundary 展示
@@ -73,7 +73,7 @@ export class TableColumnModel extends DisplayItemModel {
async afterAddAsSubModel() {
await super.afterAddAsSubModel();
await this.applyAutoFlows();
await this.dispatchEvent('beforeRender');
}
static defineChildren(ctx: FlowModelContext) {
@@ -252,7 +252,7 @@ TableColumnModel.registerFlow({
ctx.model.setProps('title', collectionField.title);
ctx.model.setProps('dataIndex', collectionField.name);
// for quick edit
await ctx.model.applySubModelsAutoFlows('field');
await ctx.model.applySubModelsBeforeRenderFlows('field');
ctx.model.setProps({
...collectionField.getComponentProps(),
});
@@ -166,7 +166,7 @@ export class SubTableColumnModel<
async afterAddAsSubModel() {
await super.afterAddAsSubModel();
await this.applyAutoFlows();
await this.dispatchEvent('beforeRender');
}
getColumnProps(): TableColumnProps {
@@ -365,7 +365,7 @@ SubTableColumnModel.registerFlow({
},
},
});
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
},
},
title: {
@@ -139,7 +139,7 @@ SubTableFieldModel.registerFlow({
},
init: {
async handler(ctx, params) {
await ctx.model.applySubModelsAutoFlows('columns');
await ctx.model.applySubModelsBeforeRenderFlows('columns');
},
},
allowAddNew: {
@@ -52,7 +52,7 @@ export class DisplaySubTableFieldModel extends FieldModel {
async afterAddAsSubModel() {
await super.afterAddAsSubModel();
await this.applyAutoFlows();
await this.dispatchEvent('beforeRender');
}
getColumns() {
@@ -105,7 +105,7 @@ DisplaySubTableFieldModel.registerFlow({
steps: {
init: {
async handler(ctx) {
await ctx.model.applySubModelsAutoFlows('columns');
await ctx.model.applySubModelsBeforeRenderFlows('columns');
},
},
},
@@ -79,6 +79,7 @@ const JSFormRuntime: React.FC<{
* - 子节点由脚本渲染(DOM 操作),用于完全自定义输入体验。
*/
export class JSEditableFieldModel extends FieldModel {
private _mountedOnce = false;
render() {
return (
<JSFormRuntime
@@ -90,6 +91,21 @@ export class JSEditableFieldModel extends FieldModel {
/>
);
}
/**
* 在 CreateForm / 提交后重置等场景下,字段可能被卸载并重新挂载。
* 由于 beforeRender 命中缓存,新挂载的容器不会自动写入 JS 渲染内容。
* 这里在二次挂载时检测容器已就绪,主动触发 rerender 来刷新 beforeRender(禁用缓存),
* 保证输入框不会因为容器变更而空白。
*/
protected onMount() {
if (this._mountedOnce) {
if (this.context.ref?.current) {
this.rerender();
}
}
this._mountedOnce = true;
}
}
JSEditableFieldModel.define({
@@ -83,7 +83,7 @@ export class JSFieldModel extends FieldModel {
}
/**
* 组件挂载后,如果容器引用已就绪,强制重跑一次自动流程以把 HTML 写入当前 DOM。
* 组件挂载后,如果容器引用已就绪,强制重跑一次 beforeRender flows 以把 HTML 写入当前 DOM。
* 解决新增后因命中缓存或执行时机导致内容未写入的问题(与 JSBlock/JSItem 一致处理)。
*/
protected onMount() {
@@ -49,7 +49,7 @@ export class JSItemModel extends CommonItemModel {
}
/**
* 组件挂载后,如果容器引用已就绪,强制重跑一次自动流程以把 HTML 写入当前 DOM。
* 组件挂载后,如果容器引用已就绪,强制重跑一次 beforeRender 以把 HTML 写入当前 DOM。
* 解决某些情况下命中旧缓存导致新节点未写入而显示空白的问题。
*/
protected onMount() {
@@ -13,7 +13,7 @@ import { FlowEngine } from './flowEngine';
* ViewScopedFlowEngine(视图作用域引擎)
*
* 设计目标:
* - 仅在视图(dialog/drawer/embed)作用域内隔离“模型实例表”和“自动流缓存”;
* - 仅在视图(dialog/drawer/embed)作用域内隔离“模型实例表”和“事件缓存”;
* - 其余能力(动作/事件/模型类/资源/仓库/设置/日志/翻译等)全部直接代理到父引擎;
* - 使用 Proxy 实现“最小代理”,保持与父引擎的全局一致性。
*
@@ -59,7 +59,7 @@ describe('ViewScopedFlowEngine', () => {
expect(child.getModel(uid)).toBe(cm);
});
it('isolates auto flow cache across engines with identical model uid', async () => {
it('isolates beforeRender event cache across engines with identical model uid', async () => {
const parent = new FlowEngine();
const child = createViewScopedEngine(parent);
@@ -81,11 +81,11 @@ describe('ViewScopedFlowEngine', () => {
const pm = parent.createModel<CounterModel>({ use: CounterModel, uid });
const cm = child.createModel<CounterModel>({ use: CounterModel, uid });
await pm.applyAutoFlows();
await pm.dispatchEvent('beforeRender');
expect(count).toBe(1);
// If cache was shared, the next call would hit cache and not increment.
await cm.applyAutoFlows();
await cm.dispatchEvent('beforeRender');
expect(count).toBe(2);
});
@@ -20,7 +20,6 @@ const flowModelRendererPropKeys: (keyof FlowModelRendererProps)[] = [
'flowSettingsVariant',
'hideRemoveInSettings',
'showTitle',
'skipApplyAutoFlows',
'inputArgs',
'showErrorFallback',
'settingsMenuLevel',
@@ -87,10 +87,7 @@ export interface FlowModelRendererProps {
/** 是否在边框左上角显示模型title,默认 false */
showTitle?: boolean; // 默认 false
/** 是否跳过自动应用流程,默认 false */
skipApplyAutoFlows?: boolean; // 默认 false
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的额外上下文 */
/** 传递给 beforeRender 事件的运行时参数 */
inputArgs?: Record<string, any>;
/** 是否在最外层包装 FlowErrorFallback 组件,默认 false */
@@ -168,57 +165,7 @@ const FlowModelRendererWithAutoFlows: React.FC<{
},
);
/**
* 内部组件:不带 useApplyAutoFlows 的渲染器
*/
const FlowModelRendererWithoutAutoFlows: React.FC<{
model: FlowModel;
showFlowSettings:
| boolean
| {
showBackground?: boolean;
showBorder?: boolean;
showDragHandle?: boolean;
style?: React.CSSProperties;
/**
* @default 'inside'
*/
toolbarPosition?: 'inside' | 'above';
};
flowSettingsVariant: string;
hideRemoveInSettings: boolean;
showTitle: boolean;
showErrorFallback?: boolean;
settingsMenuLevel?: number;
extraToolbarItems?: ToolbarItemConfig[];
}> = observer(
({
model,
showFlowSettings,
flowSettingsVariant,
hideRemoveInSettings,
showTitle,
showErrorFallback,
settingsMenuLevel,
extraToolbarItems,
}) => {
setAutoFlowError(model, null);
return (
<FlowModelProvider model={model}>
<FlowModelRendererCore
model={model}
showFlowSettings={showFlowSettings}
flowSettingsVariant={flowSettingsVariant}
hideRemoveInSettings={hideRemoveInSettings}
showTitle={showTitle}
showErrorFallback={showErrorFallback}
settingsMenuLevel={settingsMenuLevel}
extraToolbarItems={extraToolbarItems}
/>
</FlowModelProvider>
);
},
);
// 移除不带 beforeRender 执行的渲染器,统一触发 beforeRender 事件
/**
* 核心渲染逻辑组件
@@ -264,7 +211,7 @@ const FlowModelRendererCore: React.FC<{
const ContentOrError: React.FC = () => {
const autoError = getAutoFlowError(model);
if (autoError) {
// 把自动流的错误转化为内容区错误,由内层边界兜住
// 将 beforeRender 事件错误转化为内容区错误,由内层边界兜住
throw autoError;
}
const rendered = model.render();
@@ -355,8 +302,7 @@ const FlowModelRendererCore: React.FC<{
* @param {string} props.flowSettingsVariant - The interaction style for flow settings.
* @param {boolean} props.hideRemoveInSettings - Whether to hide remove button in settings.
* @param {boolean} props.showTitle - Whether to show model title in the top-left corner of the border.
* @param {boolean} props.skipApplyAutoFlows - Whether to skip applying auto flows.
* @param {any} props.inputArgs - Runtime arguments to pass to useApplyAutoFlows when skipApplyAutoFlows is false.
* @param {any} props.inputArgs - Runtime arguments to pass to beforeRender event flows.
* @param {number} props.settingsMenuLevel - Settings menu levels: 1=current model only (default), 2=include sub-models.
* @param {ToolbarItemConfig[]} props.extraToolbarItems - Extra toolbar items to add to this renderer instance.
* @returns {React.ReactNode | null} The rendered output of the model, or null if the model or its render method is invalid.
@@ -369,7 +315,6 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
flowSettingsVariant = 'dropdown',
hideRemoveInSettings = false,
showTitle = false,
skipApplyAutoFlows = false,
inputArgs,
showErrorFallback = true,
settingsMenuLevel,
@@ -381,19 +326,8 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
return null;
}
// 构建渲染内容
const content = skipApplyAutoFlows ? (
<FlowModelRendererWithoutAutoFlows
model={model}
showFlowSettings={showFlowSettings}
flowSettingsVariant={flowSettingsVariant}
hideRemoveInSettings={hideRemoveInSettings}
showTitle={showTitle}
showErrorFallback={showErrorFallback}
settingsMenuLevel={settingsMenuLevel}
extraToolbarItems={extraToolbarItems}
/>
) : (
// 构建渲染内容:统一在渲染前触发 beforeRender 事件(带缓存)
const content = (
<FlowModelRendererWithAutoFlows
model={model}
showFlowSettings={showFlowSettings}
@@ -124,7 +124,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
<ConfigProvider>
<App>
<FlowEngineProvider engine={engine}>
<FlowModelRenderer model={model} showFlowSettings showErrorFallback skipApplyAutoFlows />
<FlowModelRenderer model={model} showFlowSettings showErrorFallback />
</FlowEngineProvider>
</App>
</ConfigProvider>,
@@ -141,7 +141,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
return (
<div>
{items.map((m: FlowModel) => (
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback skipApplyAutoFlows />
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback />
))}
</div>
);
@@ -167,7 +167,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
<ConfigProvider>
<App>
<FlowEngineProvider engine={engine}>
<FlowModelRenderer model={parent} skipApplyAutoFlows />
<FlowModelRenderer model={parent} />
</FlowEngineProvider>
</App>
</ConfigProvider>,
@@ -186,7 +186,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
return (
<div>
{cells.map((m: FlowModel) => (
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback skipApplyAutoFlows />
<FlowModelRenderer key={m.uid} model={m} showFlowSettings showErrorFallback />
))}
</div>
);
@@ -212,7 +212,7 @@ describe('Delete problematic model via FlowSettings menu', () => {
<ConfigProvider>
<App>
<FlowEngineProvider engine={engine}>
<FlowModelRenderer model={parent} skipApplyAutoFlows />
<FlowModelRenderer model={parent} />
</FlowEngineProvider>
</App>
</ConfigProvider>,
@@ -11,6 +11,7 @@ import _ from 'lodash';
import { FlowRuntimeContext } from '../flowContext';
import { FlowEngine } from '../flowEngine';
import type { FlowModel } from '../models';
import type { DispatchEventOptions } from '../types';
import type { ActionDefinition, ApplyFlowCacheEntry, StepDefinition } from '../types';
import { FlowExitException, resolveDefaultParams } from '../utils';
import { FlowExitAllException } from '../utils/exceptions';
@@ -19,6 +20,39 @@ import { setupRuntimeContextSteps } from '../utils/setupRuntimeContextSteps';
export class FlowExecutor {
constructor(private readonly engine: FlowEngine) {}
/** Cache wrapper for applyFlow cache lifecycle */
private async withApplyFlowCache<T>(cacheKey: string | null, executor: () => Promise<T>): Promise<T> {
if (!cacheKey || !this.engine) return await executor();
const cachedEntry = this.engine.applyFlowCache.get(cacheKey);
if (cachedEntry) {
if (cachedEntry.status === 'resolved') return cachedEntry.data as T;
if (cachedEntry.status === 'rejected') throw cachedEntry.error;
if (cachedEntry.status === 'pending') return await cachedEntry.promise;
}
const promise = executor()
.then((result) => {
this.engine.applyFlowCache.set(cacheKey, {
status: 'resolved',
data: result,
promise: Promise.resolve(result),
} as ApplyFlowCacheEntry);
return result;
})
.catch((err) => {
this.engine.applyFlowCache.set(cacheKey, {
status: 'rejected',
error: err,
promise: Promise.reject(err),
} as ApplyFlowCacheEntry);
throw err;
});
this.engine.applyFlowCache.set(cacheKey, { status: 'pending', promise } as ApplyFlowCacheEntry);
return await promise;
}
/**
* Execute a single flow on model.
*/
@@ -46,29 +80,31 @@ export class FlowExecutor {
let lastResult: any;
const stepResults: Record<string, any> = flowContext.stepResults;
// Build steps with optional eventStep injection (when flow.on is configured)
let eventStep = model.getEvent(typeof flow.on === 'string' ? flow.on : (flow.on as any)?.eventName);
if (eventStep) {
eventStep = { ...eventStep }; // clone to avoid side effects
eventStep.defaultParams = { ..._.get(flow, 'on.defaultParams', {}), ...eventStep.defaultParams };
eventStep = { ...eventStep } as any; // clone to avoid side effects
(eventStep as any).defaultParams = {
..._.get(flow, 'on.defaultParams', {}),
...(eventStep as any).defaultParams,
};
}
// Execute the event step first since it's usually the trigger condition - if the condition is not met, subsequent steps don't need to execute
const stepDefs = eventStep ? { eventStep, ...flow.steps } : flow.steps; // Record<string, StepDefinition>
const stepDefs: Record<string, StepDefinition> = eventStep
? { eventStep: eventStep as any, ...flow.steps }
: flow.steps;
// Setup steps meta and runtime mapping
setupRuntimeContextSteps(flowContext, stepDefs, model, flowKey);
const stepsRuntime = flowContext.steps as Record<string, { params: any; uiSchema?: any; result?: any }>;
for (const stepKey in stepDefs) {
if (!Object.prototype.hasOwnProperty.call(stepDefs, stepKey)) continue;
const step: StepDefinition = stepDefs[stepKey];
for (const [stepKey, step] of Object.entries(stepDefs) as [string, StepDefinition][]) {
// Resolve handler and params
let handler: ActionDefinition['handler'] | undefined;
let combinedParams: Record<string, any> = {};
let actionDefinition: ActionDefinition | undefined;
let useRawParams: StepDefinition['useRawParams'] = step.useRawParams;
if (step.use) {
// Step references a registered action
actionDefinition = model.getAction(step.use);
const actionDefinition = model.getAction(step.use);
if (!actionDefinition) {
flowContext.logger.error(
`BaseModel.applyFlow: Action '${step.use}' not found for step '${stepKey}' in flow '${flowKey}'. Skipping.`,
@@ -102,7 +138,6 @@ export class FlowExecutor {
if (!useRawParams) {
combinedParams = await flowContext.resolveJsonTemplate(combinedParams);
}
try {
if (!handler) {
flowContext.logger.error(
@@ -114,9 +149,8 @@ export class FlowExecutor {
const isAwait = step.isAwait !== false;
lastResult = isAwait ? await currentStepResult : currentStepResult;
// Store step result
// Store step result and update context
stepResults[stepKey] = lastResult;
// update the context
stepsRuntime[stepKey].result = stepResults[stepKey];
} catch (error) {
if (error instanceof FlowExitException) {
@@ -125,7 +159,6 @@ export class FlowExecutor {
}
if (error instanceof FlowExitAllException) {
flowContext.logger.info(`[FlowEngine] ${error.message}`);
// 传递特殊控制信号,让上层可中止后续流程
return Promise.resolve(error);
}
flowContext.logger.error(
@@ -138,119 +171,132 @@ export class FlowExecutor {
return Promise.resolve(stepResults);
}
/**
* Execute all auto-apply flows for model.
*/
async runAutoFlows(model: FlowModel, inputArgs?: Record<string, any>, useCache = true): Promise<any[]> {
const autoApplyFlows = model.getAutoFlows();
if (autoApplyFlows.length === 0) {
model.context.logger.warn(`FlowModel: No auto-apply flows found for model '${model.uid}'`);
return [];
}
const cacheKey = useCache
? FlowEngine.generateApplyFlowCacheKey(model.getAutoFlowCacheScope(), 'all', model.uid)
: null;
if (cacheKey && this.engine) {
const cachedEntry = this.engine.applyFlowCache.get(cacheKey);
if (cachedEntry) {
if (cachedEntry.status === 'resolved') {
model.context.logger.debug(`[FlowEngine.applyAutoFlows] Using cached result for model: ${model.uid}`);
return cachedEntry.data;
}
if (cachedEntry.status === 'rejected') throw cachedEntry.error;
if (cachedEntry.status === 'pending') return await cachedEntry.promise;
}
}
const executeAutoFlows = async (): Promise<any[]> => {
const results: any[] = [];
const runId = `${model.uid}-autoFlow-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const logger = model.context.logger.child({ module: 'flow-engine', component: 'FlowExecutor', runId });
logger.debug(
`[FlowExecutor] runAutoFlows: uid=${model.uid}, isFork=${
(model as any)?.isFork === true
}, useCache=${useCache}, runId=${runId}, flows=${autoApplyFlows.map((f) => f.key).join(',')}`,
);
try {
if (autoApplyFlows.length === 0) {
logger.warn(`FlowModel: No auto-apply flows found for model '${model.uid}'`);
} else {
for (const flow of autoApplyFlows) {
try {
logger.debug(`[FlowExecutor] runFlow: uid=${model.uid}, flowKey=${flow.key}`);
const result = await this.runFlow(model, flow.key, inputArgs, runId);
if (result instanceof FlowExitAllException) {
logger.debug(`[FlowEngine.applyAutoFlows] ${result.message}`);
break; // 终止后续流程执行
}
results.push(result);
} catch (error) {
logger.error({ err: error }, `FlowModel.applyAutoFlows: Error executing auto-apply flow '${flow.key}':`);
throw error;
}
}
}
return results;
} catch (error) {
if (error instanceof FlowExitException) {
logger.debug(`[FlowEngine.applyAutoFlows] ${error.message}`);
return results;
}
throw error;
}
};
if (!cacheKey || !this.engine) {
return await executeAutoFlows();
}
const promise = executeAutoFlows()
.then((result) => {
this.engine.applyFlowCache.set(cacheKey, {
status: 'resolved',
data: result,
promise: Promise.resolve(result),
} as ApplyFlowCacheEntry);
return result;
})
.catch((err) => {
this.engine.applyFlowCache.set(cacheKey, {
status: 'rejected',
error: err,
promise: Promise.reject(err),
} as ApplyFlowCacheEntry);
throw err;
});
this.engine.applyFlowCache.set(cacheKey, { status: 'pending', promise } as ApplyFlowCacheEntry);
return await promise;
}
// runAutoFlows 已移除:统一通过 dispatchEvent('beforeRender') + useCache 控制
/**
* Dispatch an event to flows bound via flow.on and execute them.
*/
async dispatchEvent(model: FlowModel, eventName: string, inputArgs?: Record<string, any>): Promise<void> {
const flows = Array.from(model.getFlows().values()).filter((flow) => {
const on = flow.on;
if (!on) return false;
if (typeof on === 'string') return on === eventName;
if (typeof on === 'object') return on.eventName === eventName;
return false;
});
async dispatchEvent(
model: FlowModel,
eventName: string,
inputArgs?: Record<string, any>,
options?: DispatchEventOptions,
): Promise<any> {
const isBeforeRender = eventName === 'beforeRender';
// 由模型层决定缺省值;执行器仅按显式 options 执行
const sequential = !!options?.sequential;
const useCache = !!options?.useCache;
// beforeRender 特殊处理:出错时一律抛出(用于错误边界捕获)
const throwOnError = isBeforeRender;
const runId = `${model.uid}-${eventName}-${Date.now()}`;
const logger = model.context.logger;
const promises = flows.map((flow) => {
logger.debug(`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`);
return this.runFlow(model, flow.key, inputArgs, runId).catch((error) => {
logger.error(
{ err: error },
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`,
);
});
});
await Promise.all(promises);
try {
await model.onDispatchEventStart?.(eventName, options, inputArgs);
} catch (err) {
if (isBeforeRender && err instanceof FlowExitException) {
logger.debug(`[FlowModel.dispatchEvent] ${err.message}`);
return [];
}
// 进入错误钩子并记录
try {
await model.onDispatchEventError?.(eventName, options, inputArgs, err as Error);
} finally {
logger.error({ err }, `BaseModel.dispatchEvent: Start hook error for event '${eventName}'`);
}
if (throwOnError) throw err;
return;
}
// 构造待执行的 flows 列表
const flows = isBeforeRender
? model.getEventFlows('beforeRender') // beforeRender 事件集合(兼容未声明 on 且非 manual 的定义)
: Array.from(model.getFlows().values()).filter((flow) => {
const on = flow.on;
if (!on) return false;
if (typeof on === 'string') return on === eventName;
if (typeof on === 'object') return on.eventName === eventName;
return false;
});
// 组装执行函数(返回值用于缓存;beforeRender 返回 results:any[],其它返回 true)
const execute = async () => {
if (sequential) {
const ordered = flows.slice().sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
const results: any[] = [];
for (const flow of ordered) {
try {
logger.debug(
`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}' (sequential).`,
);
const result = await this.runFlow(model, flow.key, inputArgs, runId);
if (result instanceof FlowExitAllException) {
logger.debug(`[FlowEngine.dispatchEvent] ${result.message}`);
break; // 终止后续
}
results.push(result);
} catch (error) {
logger.error(
{ err: error },
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}' (sequential):`,
);
throw error;
}
}
return results;
}
// 并行
const results = await Promise.all(
flows.map(async (flow) => {
logger.debug(`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`);
try {
return await this.runFlow(model, flow.key, inputArgs, runId);
} catch (error) {
logger.error(
{ err: error },
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`,
);
if (throwOnError) throw error;
return undefined;
}
}),
);
return results.filter((x) => x !== undefined);
};
// 缓存键:按事件+scope 统一管理(beforeRender 也使用事件名 beforeRender)
const argsKey = useCache ? JSON.stringify(inputArgs ?? {}) : '';
const cacheKey = useCache
? FlowEngine.generateApplyFlowCacheKey(
`event:${model.getFlowCacheScope(eventName)}:${argsKey}`,
eventName,
model.uid,
)
: null;
try {
const result = await this.withApplyFlowCache(cacheKey, execute);
// 事件结束钩子
try {
await model.onDispatchEventEnd?.(eventName, options, inputArgs, result);
} catch (hookErr) {
logger.error({ err: hookErr }, `BaseModel.dispatchEvent: End hook error for event '${eventName}'`);
}
return result;
} catch (error) {
// 进入错误钩子并记录
try {
await model.onDispatchEventError?.(eventName, options, inputArgs, error as Error);
} catch (_) {
// swallow secondary hook error
}
model.context.logger.error(
{ err: error },
`BaseModel.dispatchEvent: Error executing event '${eventName}' for model '${model.uid}':`,
);
if (throwOnError) throw error;
}
}
}
@@ -81,7 +81,7 @@ describe('FlowExecutor', () => {
expect(result.step2).toBe('step2-ok');
});
it('runAutoFlows executes only auto flows in sort order and caches result', async () => {
it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => {
const calls: string[] = [];
const mkFlow = (key: string, sort: number) => ({
sort,
@@ -104,15 +104,21 @@ describe('FlowExecutor', () => {
const model = createModelWithFlows('m-auto', flows);
const r1 = await engine.executor.runAutoFlows(model);
expect(r1).toHaveLength(2);
const r1 = await engine.executor.dispatchEvent(model, 'beforeRender', undefined, {
sequential: true,
useCache: true,
});
expect(Array.isArray(r1)).toBe(true);
// sort order: f1 then f2
expect(calls).toEqual(['f1', 'f2']);
// second run should hit cache and not execute handlers again
const prevCount = calls.length;
const r2 = await engine.executor.runAutoFlows(model);
expect(r2).toEqual(r1);
const r2 = await engine.executor.dispatchEvent(model, 'beforeRender', undefined, {
sequential: true,
useCache: true,
});
expect(Array.isArray(r2)).toBe(true);
expect(calls.length).toBe(prevCount);
});
@@ -133,6 +139,146 @@ describe('FlowExecutor', () => {
expect(submitHandler).not.toHaveBeenCalled();
});
it('dispatchEvent default parallel does not stop on exitAll', async () => {
const calls: string[] = [];
const mkFlow = (key: string, opts?: { exitAll?: boolean }) => ({
on: { eventName: 'par' },
steps: {
only: {
handler: vi.fn().mockImplementation((ctx) => {
calls.push(key);
if (opts?.exitAll) ctx.exitAll();
}),
},
},
});
const flows = {
a: mkFlow('a', { exitAll: true }),
b: mkFlow('b'),
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-par', flows);
await engine.executor.dispatchEvent(model, 'par');
// 在并行模式下,b 不会被 exitAll 中断
expect(calls.sort()).toEqual(['a', 'b']);
});
it('dispatchEvent sequential respects sort order and stops on errors', async () => {
const calls: string[] = [];
const mkFlow = (key: string, sort: number, opts?: { throw?: boolean }) => ({
on: { eventName: 'seq2' },
sort,
steps: {
only: {
handler: vi.fn().mockImplementation(() => {
calls.push(key);
if (opts?.throw) throw new Error('boom');
}),
},
},
});
const flows = {
f2: mkFlow('f2', 2),
f1: mkFlow('f1', 1, { throw: true }),
f3: mkFlow('f3', 3),
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-seq2', flows);
await engine.executor.dispatchEvent(model, 'seq2', {}, { sequential: true });
// 顺序:按 sort 执行;f1 抛错后终止,f2 和 f3 不会执行
expect(calls).toEqual(['f1']);
});
it('dispatchEvent caches non-beforeRender when useCache=true', async () => {
const handler = vi.fn();
const flows = {
onFoo: { on: 'foo', steps: { s: { handler } } },
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-evt-cache', flows);
await engine.executor.dispatchEvent(model, 'foo', {}, { useCache: true });
await engine.executor.dispatchEvent(model, 'foo', {}, { useCache: true });
expect(handler).toHaveBeenCalledTimes(1);
});
it("dispatchEvent('beforeRender') runs once due to cache (when options specify)", async () => {
const handler = vi.fn();
const flows = {
auto1: { steps: { s: { handler } } }, // 无 on → 视为 beforeRender
auto2: { on: 'beforeRender', steps: { s: { handler } } },
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-br-cache', flows);
await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
// 两次触发但步骤处理只执行一次(命中缓存)
expect(handler).toHaveBeenCalledTimes(2); // 每个 flow 各 1 次,共 2 次
});
it('dispatchEvent supports sequential execution order and exitAll break', async () => {
const calls: string[] = [];
const mkFlow = (key: string, sort: number, opts?: { exitAll?: boolean }) => ({
on: { eventName: 'seq' },
sort,
steps: {
only: {
handler: vi.fn().mockImplementation((ctx) => {
calls.push(key);
if (opts?.exitAll) ctx.exitAll();
}),
},
},
});
const flows = {
f1: mkFlow('f1', 1, { exitAll: true }),
f2: mkFlow('f2', 2),
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-seq', flows);
await engine.executor.dispatchEvent(model, 'seq', {}, { sequential: true });
// f1 executed then exitAll stops f2
expect(calls).toEqual(['f1']);
});
it("dispatchEvent('beforeRender') caches results (key includes eventName) when options specify", async () => {
const handler = vi.fn().mockResolvedValue('ok');
const flows = {
f1: { steps: { s: { handler } } },
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-br', flows);
await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
// handler only executed once because of cache
expect(handler).toHaveBeenCalledTimes(1);
});
it("dispatchEvent('beforeRender') rejects when a step throws", async () => {
const handler = vi.fn().mockImplementation(() => {
throw new Error('boom-br');
});
const flows = {
bad: { steps: { s: { handler } } },
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
const model = createModelWithFlows('m-br-reject', flows);
await expect(engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true })).rejects.toThrow(
'boom-br',
);
});
it('instance flow overrides global flow with same key', async () => {
class MyModel extends FlowModel {}
+2 -2
View File
@@ -130,7 +130,7 @@ export class FlowEngine {
*/
public reactView: ReactView;
/**
* Flow executor that runs flows and auto-flows.
* Flow executor that runs event flows.
*/
public executor: FlowExecutor;
@@ -803,7 +803,7 @@ export class FlowEngine {
// 7. 触发事件以通知其他部分模型已替换
if (currentParent) {
currentParent.emitter.setPaused(false);
currentParent.parent.invalidateAutoFlowCache(true);
currentParent.parent.invalidateFlowCache('beforeRender', true);
currentParent.parent?.rerender();
currentParent.emitter.emit('onSubModelReplaced', { oldModel, newModel });
}
@@ -35,7 +35,9 @@ export function useApplyAutoFlows(
const { loading, error } = useRequest(
async () => {
await model.applyAutoFlows(inputArgs);
if (!model) return;
// beforeRender 在模型层默认顺序执行并默认使用缓存(可覆盖)
await model.dispatchEvent('beforeRender', inputArgs);
},
{
refreshDeps: [model, inputArgs],
@@ -10,13 +10,13 @@
import { describe, expect, test, beforeEach } from 'vitest';
import { FlowModel } from '@nocobase/flow-engine';
describe('FlowModel.getFlows sorting and getAutoFlows order', () => {
describe('FlowModel.getFlows sorting and getEventFlows(beforeRender) order', () => {
let TestFlowModel: typeof FlowModel;
let model: FlowModel;
beforeEach(() => {
// use a fresh subclass to isolate class-level registries
TestFlowModel = class extends FlowModel {};
TestFlowModel = class extends FlowModel<any> {};
// provide a minimal fake engine to avoid circular deps in tests
const fakeEngine = { getModel: () => undefined } as any;
model = new TestFlowModel({ flowEngine: fakeEngine } as any);
@@ -37,7 +37,7 @@ describe('FlowModel.getFlows sorting and getAutoFlows order', () => {
expect(orderedKeys).toEqual(['flowD', 'flowB', 'flowC', 'flowA']);
});
test('getAutoFlows keeps getFlows order and filters out manual/on flows', () => {
test("getEventFlows('beforeRender') keeps getFlows order and filters out manual/on flows", () => {
// class-level
TestFlowModel.registerFlow('flowA', { title: 'A', sort: 10, steps: {} });
TestFlowModel.registerFlow('flowB', { title: 'B', sort: 5, steps: {} });
@@ -51,7 +51,7 @@ describe('FlowModel.getFlows sorting and getAutoFlows order', () => {
model.registerFlow('manualFlow', { title: 'Manual', manual: true, sort: -1, steps: {} });
const getFlowsOrder = Array.from(model.getFlows().keys());
const autoFlowKeys = model.getAutoFlows().map((f) => f.key);
const autoFlowKeys = model.getEventFlows('beforeRender').map((f) => f.key);
// auto flows should exclude event/manual flows
expect(autoFlowKeys).toEqual(['flowD', 'flowB', 'flowC', 'flowE', 'flowA']);
@@ -7,18 +7,17 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { autorun, observable, reaction, reactive } from '@nocobase/flow-engine';
import { APIClient } from '@nocobase/sdk';
import { reaction } from '@nocobase/flow-engine';
import { render, waitFor } from '@testing-library/react';
import React from 'react';
import { vi } from 'vitest';
import { FlowModelRenderer } from '../../components/FlowModelRenderer';
import { FlowEngineProvider } from '../../provider';
import { FlowEngine } from '../../flowEngine';
import type { DefaultStructure, FlowDefinitionOptions, FlowModelOptions, ModelConstructor } from '../../types';
import type { DefaultStructure, FlowDefinitionOptions, FlowModelOptions } from '../../types';
import { FlowExitException } from '../../utils';
import { FlowExitAllException } from '../../utils/exceptions';
import { defineFlow, FlowModel, ModelRenderMode } from '../flowModel';
import { ForkFlowModel } from '../forkFlowModel';
import { FlowModel, ModelRenderMode } from '../flowModel';
// 全局处理测试中的未处理 Promise rejection
const originalUnhandledRejection = process.listeners('unhandledRejection');
@@ -392,7 +391,7 @@ describe('FlowModel', () => {
TestFlowModel.registerFlow(exitFlow2);
const loggerSpy = vi.spyOn(model.context.logger, 'info').mockImplementation(() => {});
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
expect(exitFlow2.steps.step2.handler).toHaveBeenCalled();
@@ -430,7 +429,7 @@ describe('FlowModel', () => {
TestFlowModel.registerFlow(exitFlow2);
const loggerSpy = vi.spyOn(model.context.logger, 'info').mockImplementation(() => {});
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
expect(exitFlow2.steps.step2.handler).not.toHaveBeenCalled();
@@ -530,7 +529,7 @@ describe('FlowModel', () => {
});
});
describe('applyAutoFlows', () => {
describe('beforeRender flows', () => {
test('should execute all auto flows', async () => {
const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1', sort: 1 };
const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2', sort: 2 };
@@ -540,7 +539,7 @@ describe('FlowModel', () => {
TestFlowModel.registerFlow(autoFlow2);
TestFlowModel.registerFlow(manualFlow);
const results = await model.applyAutoFlows();
const results = await model.dispatchEvent('beforeRender');
expect(results).toHaveLength(2);
expect(autoFlow1.steps.autoStep.handler).toHaveBeenCalled();
@@ -594,13 +593,13 @@ describe('FlowModel', () => {
TestFlowModel.registerFlow(autoFlow2);
TestFlowModel.registerFlow(autoFlow3);
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
expect(executionOrder).toEqual(['auto2', 'auto3', 'auto1']);
});
test('should no results when no auto flows found', async () => {
const results = await model.applyAutoFlows();
const results = await model.dispatchEvent('beforeRender');
expect(results).toEqual([]);
// Note: Log output may be captured in stderr, not console.log
@@ -617,16 +616,14 @@ describe('FlowModel', () => {
afterHookSpy = vi.fn();
errorHookSpy = vi.fn();
TestFlowModelWithHooks = class extends TestFlowModel {
async onBeforeAutoFlows(inputArgs?: Record<string, any>) {
beforeHookSpy(inputArgs);
async onDispatchEventStart(eventName: string, _opts?: any, inputArgs?: Record<string, any>) {
if (eventName === 'beforeRender') beforeHookSpy(inputArgs);
}
async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>) {
afterHookSpy(results, inputArgs);
async onDispatchEventEnd(eventName: string, _opts?: any, inputArgs?: Record<string, any>, results?: any[]) {
if (eventName === 'beforeRender') afterHookSpy(results, inputArgs);
}
async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>) {
errorHookSpy(error, inputArgs);
async onDispatchEventError(eventName: string, _opts?: any, inputArgs?: Record<string, any>, error?: Error) {
if (eventName === 'beforeRender') errorHookSpy(error, inputArgs);
}
};
});
@@ -638,7 +635,7 @@ describe('FlowModel', () => {
const modelWithHooks = new TestFlowModelWithHooks(modelOptions);
const inputArgs = { test: 'value' };
const results = await modelWithHooks.applyAutoFlows(inputArgs);
const results = await modelWithHooks.dispatchEvent('beforeRender', inputArgs);
// Verify hooks were called
expect(beforeHookSpy).toHaveBeenCalledTimes(1);
@@ -654,22 +651,22 @@ describe('FlowModel', () => {
);
});
test('should allow onBeforeAutoFlows to terminate flow via ctx.exit()', async () => {
test("should allow onDispatchEventStart('beforeRender') to terminate flow via ctx.exit()", async () => {
const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1' };
const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2' };
const TestFlowModelWithExitHooks = class extends TestFlowModel {
async onBeforeAutoFlows(inputArgs?: Record<string, any>) {
beforeHookSpy(inputArgs);
throw new FlowExitException('autoFlows', this.uid);
async onDispatchEventStart(eventName: string, _opts?: any, inputArgs?: Record<string, any>) {
if (eventName === 'beforeRender') {
beforeHookSpy(inputArgs);
throw new FlowExitException('beforeRender', this.uid);
}
}
async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>) {
afterHookSpy(results, inputArgs);
async onDispatchEventEnd(eventName: string, _o?: any, _i?: any, _r?: any[]) {
if (eventName === 'beforeRender') afterHookSpy(_r, _i);
}
async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>) {
errorHookSpy(error, inputArgs);
async onDispatchEventError(eventName: string, _o?: any, i?: any, e?: Error) {
if (eventName === 'beforeRender') errorHookSpy(e, i);
}
};
@@ -678,9 +675,9 @@ describe('FlowModel', () => {
TestFlowModelWithExitHooks.registerFlow(autoFlow2);
const modelWithHooks = new TestFlowModelWithExitHooks(modelOptions);
const results = await modelWithHooks.applyAutoFlows();
const results = await modelWithHooks.dispatchEvent('beforeRender');
// Should have called onBeforeAutoFlows but not onAfterAutoFlows
// Should have called onDispatchEventStart but not onDispatchEventEnd
expect(beforeHookSpy).toHaveBeenCalledTimes(1);
expect(afterHookSpy).not.toHaveBeenCalled();
expect(errorHookSpy).not.toHaveBeenCalled();
@@ -688,12 +685,12 @@ describe('FlowModel', () => {
// Should return empty results since flow was terminated early
expect(results).toEqual([]);
// Auto flows should not have been executed
// flows should not have been executed
expect(autoFlow1.steps.autoStep.handler).not.toHaveBeenCalled();
expect(autoFlow2.steps.autoStep.handler).not.toHaveBeenCalled();
});
test('should call onAutoFlowsError when flow execution fails', async () => {
test("should call onDispatchEventError('beforeRender') when flow execution fails", async () => {
const errorFlow = {
key: 'errorFlow',
@@ -708,11 +705,8 @@ describe('FlowModel', () => {
TestFlowModelWithHooks.registerFlow(errorFlow);
const modelWithHooks = new TestFlowModelWithHooks(modelOptions);
// 测试错误处理钩子功能
await expect(modelWithHooks.applyAutoFlows()).rejects.toThrow('Test error');
// Verify hooks were called
await modelWithHooks.dispatchEvent('beforeRender');
// Verify hooks were called (error path)
expect(beforeHookSpy).toHaveBeenCalledTimes(1);
expect(afterHookSpy).not.toHaveBeenCalled();
expect(errorHookSpy).toHaveBeenCalledTimes(1);
@@ -726,14 +720,14 @@ describe('FlowModel', () => {
);
});
test('should provide access to step results in onAfterAutoFlows', async () => {
test('should provide access to step results in onDispatchEventEnd(beforeRender)', async () => {
const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1' };
const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2' };
TestFlowModelWithHooks.registerFlow(autoFlow1);
TestFlowModelWithHooks.registerFlow(autoFlow2);
const modelWithHooks = new TestFlowModelWithHooks(modelOptions);
await modelWithHooks.applyAutoFlows();
await modelWithHooks.dispatchEvent('beforeRender');
expect(afterHookSpy).toHaveBeenCalledTimes(1);
@@ -777,6 +771,82 @@ describe('FlowModel', () => {
}
});
test('getFlows includes beforeRender and no-on flows; excludes manual and other events', async () => {
const TestM = class extends FlowModel {};
const beforeHandler = vi.fn();
const noOnHandler = vi.fn();
const manualHandler = vi.fn();
TestM.registerFlow({ key: 'beforeA', on: 'beforeRender', sort: 2, steps: { s: { handler: beforeHandler } } });
TestM.registerFlow({ key: 'noOnB', sort: 1, steps: { s: { handler: noOnHandler } } });
TestM.registerFlow({ key: 'manualSkip', manual: true, steps: { s: { handler: manualHandler } } });
TestM.registerFlow({ key: 'otherEvent', on: 'click', steps: { s: { handler: vi.fn() } } });
const m = new TestM(modelOptions);
const autoKeys = m.getEventFlows('beforeRender').map((f) => f.key);
// sort 按 1,2 排序:noOnB -> beforeA
expect(autoKeys).toEqual(['noOnB', 'beforeA']);
// beforeRender 会运行两者
await m.dispatchEvent('beforeRender');
expect(beforeHandler).toHaveBeenCalledTimes(1);
expect(noOnHandler).toHaveBeenCalledTimes(1);
expect(manualHandler).not.toHaveBeenCalled();
});
test('beforeRender executes in sort order mixing no-on and beforeRender', async () => {
const TestM = class extends FlowModel {};
const calls: string[] = [];
TestM.registerFlow({ key: 'noOn1', sort: 1, steps: { s: { handler: () => calls.push('noOn1') } } });
TestM.registerFlow({
key: 'before2',
on: 'beforeRender',
sort: 2,
steps: { s: { handler: () => calls.push('before2') } },
});
TestM.registerFlow({ key: 'noOn3', sort: 3, steps: { s: { handler: () => calls.push('noOn3') } } });
const m = new TestM(modelOptions);
await m.dispatchEvent('beforeRender');
expect(calls).toEqual(['noOn1', 'before2', 'noOn3']);
});
test("model.dispatchEvent('beforeRender') uses cache keyed by eventName", async () => {
const TestM = class extends FlowModel {};
const handler = vi.fn();
TestM.registerFlow({ key: 'autoA', steps: { s: { handler } } });
const m = new TestM(modelOptions);
await m.dispatchEvent('beforeRender');
await m.dispatchEvent('beforeRender');
expect(handler).toHaveBeenCalledTimes(1);
});
test('invalidFlowCache should clear specific event cache', async () => {
const TestM = class extends FlowModel {};
const h1 = vi.fn();
const h2 = vi.fn();
TestM.registerFlow({ key: 'onFoo', on: 'foo', steps: { s: { handler: h1 } } });
TestM.registerFlow({ key: 'onBar', on: 'bar', steps: { s: { handler: h2 } } });
const m = new TestM(modelOptions);
await m.dispatchEvent('foo', {}, { useCache: true });
await m.dispatchEvent('bar', {}, { useCache: true });
// cached
await m.dispatchEvent('foo', {}, { useCache: true });
await m.dispatchEvent('bar', {}, { useCache: true });
expect(h1).toHaveBeenCalledTimes(1);
expect(h2).toHaveBeenCalledTimes(1);
// invalidate foo only
m.invalidateFlowCache('foo');
await m.dispatchEvent('foo', {}, { useCache: true });
await m.dispatchEvent('bar', {}, { useCache: true });
expect(h1).toHaveBeenCalledTimes(2);
expect(h2).toHaveBeenCalledTimes(1);
});
test('should handle multiple flows for same event', async () => {
const eventFlow1 = { ...createEventFlowDefinition('sharedEvent'), key: 'event1' };
const eventFlow2 = { ...createEventFlowDefinition('sharedEvent'), key: 'event2' };
@@ -802,9 +872,13 @@ describe('FlowModel', () => {
const _dispatchEventWithDebounceSpy = vi.spyOn(model as any, '_dispatchEventWithDebounce');
// Test with debounce enabled
await model.dispatchEvent('debouncedEvent', { data: 'test' }, { debounce: true });
await model.dispatchEvent('debouncedEvent', { data: 'test' }, { debounce: true, sequential: true });
expect(_dispatchEventWithDebounceSpy).toHaveBeenCalledWith('debouncedEvent', { data: 'test' });
expect(_dispatchEventWithDebounceSpy).toHaveBeenCalledWith(
'debouncedEvent',
{ data: 'test' },
expect.objectContaining({ sequential: true }),
);
expect(_dispatchEventSpy).not.toHaveBeenCalled();
_dispatchEventSpy.mockRestore();
@@ -821,7 +895,11 @@ describe('FlowModel', () => {
// Test with debounce disabled
await model.dispatchEvent('normalEvent', { data: 'test' }, { debounce: false });
expect(_dispatchEventSpy).toHaveBeenCalledWith('normalEvent', { data: 'test' });
expect(_dispatchEventSpy).toHaveBeenCalledWith(
'normalEvent',
{ data: 'test' },
expect.objectContaining({ sequential: undefined }),
);
expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
_dispatchEventSpy.mockRestore();
@@ -838,7 +916,11 @@ describe('FlowModel', () => {
// Test without debounce option
await model.dispatchEvent('defaultEvent', { data: 'test' });
expect(_dispatchEventSpy).toHaveBeenCalledWith('defaultEvent', { data: 'test' });
expect(_dispatchEventSpy).toHaveBeenCalledWith(
'defaultEvent',
{ data: 'test' },
expect.objectContaining({ sequential: undefined }),
);
expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
_dispatchEventSpy.mockRestore();
@@ -855,7 +937,11 @@ describe('FlowModel', () => {
// Test with undefined options
await model.dispatchEvent('undefinedOptionsEvent', { data: 'test' }, undefined);
expect(_dispatchEventSpy).toHaveBeenCalledWith('undefinedOptionsEvent', { data: 'test' });
expect(_dispatchEventSpy).toHaveBeenCalledWith(
'undefinedOptionsEvent',
{ data: 'test' },
expect.objectContaining({ sequential: undefined }),
);
expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
_dispatchEventSpy.mockRestore();
@@ -1265,41 +1351,63 @@ describe('FlowModel', () => {
});
});
describe('applySubModelsAutoFlows', () => {
describe('applySubModelsBeforeRenderFlows', () => {
test('should apply auto flows to all array subModels', async () => {
const child1 = new FlowModel({ uid: 'child1', flowEngine });
const child2 = new FlowModel({ uid: 'child2', flowEngine });
child1.applyAutoFlows = vi.fn().mockResolvedValue([]);
child2.applyAutoFlows = vi.fn().mockResolvedValue([]);
child1.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
child2.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
parentModel.addSubModel('children', child1);
parentModel.addSubModel('children', child2);
const runtimeData = { test: 'extra' };
await parentModel.applySubModelsAutoFlows('children', runtimeData);
await parentModel.applySubModelsBeforeRenderFlows('children', runtimeData);
expect(child1.applyAutoFlows).toHaveBeenCalledWith(runtimeData);
expect(child2.applyAutoFlows).toHaveBeenCalledWith(runtimeData);
expect(child1.dispatchEvent).toHaveBeenCalledWith('beforeRender', runtimeData);
expect(child2.dispatchEvent).toHaveBeenCalledWith('beforeRender', runtimeData);
});
test('should apply auto flows to single subModel', async () => {
const child = new FlowModel({ uid: 'child', flowEngine });
child.applyAutoFlows = vi.fn().mockResolvedValue([]);
child.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
parentModel.setSubModel('child', child);
const runtimeData = { test: 'extra' };
await parentModel.applySubModelsAutoFlows('child', runtimeData);
await parentModel.applySubModelsBeforeRenderFlows('child', runtimeData);
expect(child.applyAutoFlows).toHaveBeenCalledWith(runtimeData);
expect(child.dispatchEvent).toHaveBeenCalledWith('beforeRender', runtimeData);
});
test('should handle empty subModels gracefully', async () => {
await expect(parentModel.applySubModelsAutoFlows('nonExistent')).resolves.not.toThrow();
await expect(parentModel.applySubModelsBeforeRenderFlows('nonExistent')).resolves.not.toThrow();
});
});
describe('beforeRender cache invalidation on inputArgs change', () => {
test('should rerun when inputArgs changed and reuse when equal', async () => {
const TestM = class extends FlowModel {};
const handler = vi.fn();
TestM.registerFlow({ key: 'auto1', steps: { s: { handler } } });
const m = new TestM(modelOptions);
// First run with {a:1}
await m.dispatchEvent('beforeRender', { a: 1 });
expect(handler).toHaveBeenCalledTimes(1);
// Second run with same args should hit cache -> no extra handler calls
await m.dispatchEvent('beforeRender', { a: 1 });
expect(handler).toHaveBeenCalledTimes(1);
// Different args invalidate cache -> handler called again
await m.dispatchEvent('beforeRender', { a: 2 });
expect(handler).toHaveBeenCalledTimes(2);
});
});
@@ -1627,7 +1735,7 @@ describe('FlowModel', () => {
});
describe('rendering operations', () => {
test('should not pre-call render for RenderFunction mode and call exactly once on render', () => {
test('should not pre-call render for RenderFunction mode and call exactly once on render', async () => {
const renderSpy = vi.fn(() => vi.fn());
class CallbackRenderModel extends FlowModel {
@@ -1645,17 +1753,20 @@ describe('FlowModel', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const { unmount } = render(
React.createElement(FlowModelRenderer, { model: callbackModel, skipApplyAutoFlows: true }),
React.createElement(
FlowEngineProvider,
{ engine: callbackModel.flowEngine },
React.createElement(FlowModelRenderer, { model: callbackModel }),
),
);
expect(renderSpy).toHaveBeenCalledTimes(1);
await waitFor(() => expect(renderSpy).toHaveBeenCalledTimes(1));
unmount();
} finally {
warnSpy.mockRestore();
}
});
test('should not pre-call render for ReactElement mode and call exactly once on actual render', () => {
test('should not pre-call render for ReactElement mode and call exactly once on actual render', async () => {
const renderSpy = vi.fn(() => React.createElement('div', { 'data-testid': 'elt' }, 'Elt'));
class ElementRenderModel extends FlowModel {
@@ -1670,11 +1781,15 @@ describe('FlowModel', () => {
expect(renderSpy).toHaveBeenCalledTimes(0);
const { getByTestId, unmount } = render(
React.createElement(FlowModelRenderer, { model: elementModel, skipApplyAutoFlows: true }),
React.createElement(
FlowEngineProvider,
{ engine: elementModel.flowEngine },
React.createElement(FlowModelRenderer, { model: elementModel }),
),
);
// Render should be called exactly once during mount
expect(renderSpy).toHaveBeenCalledTimes(1);
await waitFor(() => expect(renderSpy).toHaveBeenCalledTimes(1));
expect(getByTestId('elt')).toBeTruthy();
unmount();
@@ -1690,13 +1805,15 @@ describe('FlowModel', () => {
expect(result.type.displayName).toBe('ReactiveWrapper(FlowModel)');
});
test('should rerender with previous auto flows', async () => {
test('should rerender triggers beforeRender without cache', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
model.applyAutoFlows = vi.fn().mockResolvedValue([]);
model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
try {
await expect(model.rerender()).resolves.not.toThrow();
expect(model.applyAutoFlows).toHaveBeenCalled();
expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
useCache: false,
});
} finally {
consoleSpy.mockRestore();
}
@@ -2039,16 +2156,16 @@ describe('FlowModel', () => {
vi.restoreAllMocks();
});
describe('invalidateAutoFlowCache', () => {
describe('invalidateFlowCache', () => {
test('should delete auto flow cache for current model', () => {
const expectedCacheKey = 'autoFlow-all-test-model-uid';
const expectedCacheKey = 'event:beforeRender-beforeRender-test-model-uid';
realFlowEngine.applyFlowCache.set(expectedCacheKey, {
status: 'resolved',
data: [],
promise: Promise.resolve([]),
});
model.invalidateAutoFlowCache();
model.invalidateFlowCache('beforeRender');
expect(deleteSpy).toHaveBeenCalledWith(expectedCacheKey);
});
@@ -2057,8 +2174,8 @@ describe('FlowModel', () => {
const fork1 = model.createFork();
const fork2 = model.createFork();
const fork1CacheKey = `${fork1.forkId}-all-test-model-uid`;
const fork2CacheKey = `${fork2.forkId}-all-test-model-uid`;
const fork1CacheKey = `event:beforeRender:${fork1.forkId}-beforeRender-test-model-uid`;
const fork2CacheKey = `event:beforeRender:${fork2.forkId}-beforeRender-test-model-uid`;
realFlowEngine.applyFlowCache.set(fork1CacheKey, {
status: 'resolved',
@@ -2071,7 +2188,7 @@ describe('FlowModel', () => {
promise: Promise.resolve([]),
});
model.invalidateAutoFlowCache();
model.invalidateFlowCache('beforeRender');
expect(deleteSpy).toHaveBeenCalledWith(fork1CacheKey);
expect(deleteSpy).toHaveBeenCalledWith(fork2CacheKey);
@@ -2081,27 +2198,27 @@ describe('FlowModel', () => {
const childModel1 = new FlowModel({ uid: 'child1', flowEngine: realFlowEngine });
const childModel2 = new FlowModel({ uid: 'child2', flowEngine: realFlowEngine });
const child1Spy = vi.spyOn(childModel1, 'invalidateAutoFlowCache');
const child2Spy = vi.spyOn(childModel2, 'invalidateAutoFlowCache');
const child1Spy = vi.spyOn(childModel1, 'invalidateFlowCache');
const child2Spy = vi.spyOn(childModel2, 'invalidateFlowCache');
model.addSubModel('children', childModel1);
model.addSubModel('children', childModel2);
model.invalidateAutoFlowCache(true);
model.invalidateFlowCache('beforeRender', true);
expect(child1Spy).toHaveBeenCalledWith(true);
expect(child2Spy).toHaveBeenCalledWith(true);
expect(child1Spy).toHaveBeenCalledWith('beforeRender', true);
expect(child2Spy).toHaveBeenCalledWith('beforeRender', true);
});
test('should recursively invalidate cache for object subModels', () => {
const childModel = new FlowModel({ uid: 'child', flowEngine: realFlowEngine });
const childSpy = vi.spyOn(childModel, 'invalidateAutoFlowCache');
const childSpy = vi.spyOn(childModel, 'invalidateFlowCache');
model.setSubModel('child', childModel);
model.invalidateAutoFlowCache(true);
model.invalidateFlowCache('beforeRender', true);
expect(childSpy).toHaveBeenCalledWith(true);
expect(childSpy).toHaveBeenCalledWith('beforeRender', true);
});
test('should handle mixed array and object subModels', () => {
@@ -2109,25 +2226,31 @@ describe('FlowModel', () => {
const arrayChild2 = new FlowModel({ uid: 'arrayChild2', flowEngine: realFlowEngine });
const objectChild = new FlowModel({ uid: 'objectChild', flowEngine: realFlowEngine });
const array1Spy = vi.spyOn(arrayChild1, 'invalidateAutoFlowCache');
const array2Spy = vi.spyOn(arrayChild2, 'invalidateAutoFlowCache');
const objectSpy = vi.spyOn(objectChild, 'invalidateAutoFlowCache');
const array1Spy = vi.spyOn(arrayChild1, 'invalidateFlowCache');
const array2Spy = vi.spyOn(arrayChild2, 'invalidateFlowCache');
const objectSpy = vi.spyOn(objectChild, 'invalidateFlowCache');
model.addSubModel('arrayChildren', arrayChild1);
model.addSubModel('arrayChildren', arrayChild2);
model.setSubModel('objectChild', objectChild);
model.invalidateAutoFlowCache(true);
model.invalidateFlowCache('beforeRender', true);
expect(array1Spy).toHaveBeenCalledWith(true);
expect(array2Spy).toHaveBeenCalledWith(true);
expect(objectSpy).toHaveBeenCalledWith(true);
expect(array1Spy).toHaveBeenCalledWith('beforeRender', true);
expect(array2Spy).toHaveBeenCalledWith('beforeRender', true);
expect(objectSpy).toHaveBeenCalledWith('beforeRender', true);
});
test('should handle empty subModels without error', () => {
model.invalidateAutoFlowCache();
realFlowEngine.applyFlowCache.set('event:beforeRender-beforeRender-test-model-uid', {
status: 'resolved',
data: [],
promise: Promise.resolve([]),
});
expect(deleteSpy).toHaveBeenCalledWith('autoFlow-all-test-model-uid');
model.invalidateFlowCache('beforeRender');
expect(deleteSpy).toHaveBeenCalledWith('event:beforeRender-beforeRender-test-model-uid');
});
test('should handle null flowEngine gracefully', () => {
@@ -2135,19 +2258,19 @@ describe('FlowModel', () => {
modelWithValidEngine.flowEngine = null;
expect(() => {
modelWithValidEngine.invalidateAutoFlowCache();
modelWithValidEngine.invalidateFlowCache('beforeRender');
}).not.toThrow();
});
test('should pass deep parameter to recursive calls', () => {
const childModel = new FlowModel({ uid: 'child', flowEngine: realFlowEngine });
const childSpy = vi.spyOn(childModel, 'invalidateAutoFlowCache');
const childSpy = vi.spyOn(childModel, 'invalidateFlowCache');
model.setSubModel('child', childModel);
model.invalidateAutoFlowCache(true);
model.invalidateFlowCache('beforeRender', true);
expect(childSpy).toHaveBeenCalledWith(true);
expect(childSpy).toHaveBeenCalledWith('beforeRender', true);
});
});
});
@@ -2198,7 +2321,7 @@ describe('FlowModel', () => {
});
const handlerSpy = flow.steps.testStep.handler as any;
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
expect(handlerSpy).toHaveBeenCalled();
});
@@ -2245,7 +2368,7 @@ describe('FlowModel', () => {
});
const handlerSpy = flow.steps.nestedStep.handler as any;
await model.applyAutoFlows();
await model.dispatchEvent('beforeRender');
expect(handlerSpy).toHaveBeenCalled();
});
});
+145 -122
View File
@@ -50,7 +50,7 @@ import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
import { GlobalFlowRegistry } from '../flow-registry/GlobalFlowRegistry';
import { FlowDefinition } from '../FlowDefinition';
import { FlowSettingsOpenOptions } from '../flowSettings';
import type { EventDefinition } from '../types';
import type { EventDefinition, FlowEvent, DispatchEventOptions } from '../types';
import { ForkFlowModel } from './forkFlowModel';
// 使用 WeakMap 为每个类缓存一个 ModelActionRegistry 实例
@@ -103,9 +103,9 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
private forkCache: Map<string, ForkFlowModel<any>> = new Map();
/**
* 上一次 applyAutoFlows 的执行参数
* 上一次 beforeRender 的执行参数
*/
private _lastAutoRunParams: any[] | null = null;
private _lastAutoRunParams: [Record<string, any> | undefined, boolean?] | null = null;
protected observerDispose: () => void;
#flowContext: FlowModelContext;
@@ -176,7 +176,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
}
if (this.flowEngine) {
this.invalidateAutoFlowCache();
this.invalidateFlowCache('beforeRender');
}
this._rerunLastAutoRun();
this.forks.forEach((fork) => {
@@ -387,13 +387,63 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
});
}
public invalidateAutoFlowCache(deep = false) {
/**
* 失效指定事件的流程缓存;未指定 eventName 时,失效当前模型全部事件缓存。
* - 默认 beforeRender 与其它事件共享同一缓存体系:prefix=`event:${scope}`,flowKey=`eventName`,uid=`model.uid`
*/
public invalidateFlowCache(eventName?: string, deep = false) {
if (this.flowEngine) {
const cacheKey = FlowEngine.generateApplyFlowCacheKey('autoFlow', 'all', this.uid);
this.flowEngine.applyFlowCache.delete(cacheKey);
const scope = `event:${this.getFlowCacheScope(eventName || 'beforeRender')}`;
const cache = this.flowEngine.applyFlowCache;
if (eventName) {
// 删除该事件名下所有缓存(忽略不同 inputArgs 的散列差异)
const altScope = scope.replace(/:/g, '-');
const uidSuffixes = [`:${this.uid}`, `-${this.uid}`];
const eventSegments = [`:${eventName}:`, `-${eventName}-`];
for (const key of cache.keys()) {
const startMatches = key.startsWith(scope) || key.startsWith(altScope);
const endMatches = uidSuffixes.some((s) => key.endsWith(s));
const eventMatches = eventSegments.some((seg) => key.includes(seg));
if (startMatches && endMatches && eventMatches) {
cache.delete(key);
}
}
} else {
// 粗粒度:扫描并删除当前模型 uid 相关的事件缓存(任意事件)
const altScope = `${scope}:`.replace(/:/g, '-');
const prefixes = [`${scope}:`, altScope];
const uidSuffixes = [`:${this.uid}`, `-${this.uid}`];
for (const key of cache.keys()) {
const startMatches = prefixes.some((p) => key.startsWith(p));
const endMatches = uidSuffixes.some((s) => key.endsWith(s));
if (startMatches && endMatches) cache.delete(key);
}
}
// 同步失效所有 fork 的缓存
this.forks.forEach((fork) => {
const forkCacheKey = FlowEngine.generateApplyFlowCacheKey(`${fork['forkId']}`, 'all', this.uid);
this.flowEngine.applyFlowCache.delete(forkCacheKey);
const forkScope = `event:${
(fork as any).getFlowCacheScope?.(eventName || 'beforeRender') ?? String((fork as any)['forkId'])
}`;
const altForkScope = forkScope.replace(/:/g, '-');
const uidSuffixes = [`:${this.uid}`, `-${this.uid}`];
if (eventName) {
const eventSegments = [`:${eventName}:`, `-${eventName}-`];
for (const key of this.flowEngine.applyFlowCache.keys()) {
const startMatches = key.startsWith(forkScope) || key.startsWith(altForkScope);
const endMatches = uidSuffixes.some((s) => key.endsWith(s));
const eventMatches = eventSegments.some((seg) => key.includes(seg));
if (startMatches && endMatches && eventMatches) this.flowEngine.applyFlowCache.delete(key);
}
} else {
const prefixes = [`${forkScope}:`, `${altForkScope}-`];
for (const key of this.flowEngine.applyFlowCache.keys()) {
const startMatches = prefixes.some((p) => key.startsWith(p));
const endMatches = uidSuffixes.some((s) => key.endsWith(s));
if (startMatches && endMatches) this.flowEngine.applyFlowCache.delete(key);
}
}
});
}
if (deep) {
@@ -402,10 +452,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
const subModelValue = this.subModels[subModelKey];
if (Array.isArray(subModelValue)) {
for (const subModel of subModelValue) {
subModel.invalidateAutoFlowCache(deep);
subModel.invalidateFlowCache(eventName, deep);
}
} else if (subModelValue instanceof FlowModel) {
subModelValue.invalidateAutoFlowCache(deep);
subModelValue.invalidateFlowCache(eventName, deep);
}
}
}
@@ -637,7 +687,11 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
return currentFlowEngine.executor.runFlow(target, flowKey, inputArgs, runId);
}
private async _dispatchEvent(eventName: string, inputArgs?: Record<string, any>): Promise<void> {
private async _dispatchEvent(
eventName: string,
inputArgs?: Record<string, any>,
options?: DispatchEventOptions,
): Promise<any[]> {
const currentFlowEngine = this.flowEngine;
if (!currentFlowEngine) {
console.warn('FlowEngine not available on this model for dispatchEvent. Please set flowEngine on the model.');
@@ -650,64 +704,73 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
this.cleanRun
}, targetIsFork=${(target as any)?.isFork === true}`,
);
await currentFlowEngine.executor.dispatchEvent(target, eventName, inputArgs);
return await currentFlowEngine.executor.dispatchEvent(target, eventName, inputArgs, options);
}
private _dispatchEventWithDebounce = _.debounce(async (eventName: string, inputArgs?: Record<string, any>) => {
return this._dispatchEvent(eventName, inputArgs);
}, 100);
private _dispatchEventWithDebounce = _.debounce(
async (eventName: string, inputArgs?: Record<string, any>, options?: DispatchEventOptions) => {
return this._dispatchEvent(eventName, inputArgs, options);
},
100,
);
async dispatchEvent(
eventName: string,
inputArgs?: Record<string, any>,
options?: {
/**
* 是否要开启防抖功能
*/
debounce: boolean;
},
): Promise<void> {
if (options?.debounce) {
return this._dispatchEventWithDebounce(eventName, inputArgs);
/** 是否要开启防抖功能 */
debounce?: boolean;
} & DispatchEventOptions,
): Promise<any[]> {
const isBeforeRender = eventName === 'beforeRender';
// 缺省值由模型层提供:beforeRender 默认顺序执行 + 使用缓存;可被 options 覆盖
const defaults = isBeforeRender ? { sequential: true, useCache: true } : {};
const execOptions = {
sequential: options?.sequential ?? (defaults as any).sequential,
useCache: options?.useCache ?? (defaults as any).useCache,
} as DispatchEventOptions;
// 记录最近一次 beforeRender 的入参与选项,便于 stepParams 变化时触发重跑
if (isBeforeRender) {
this._lastAutoRunParams = [inputArgs, execOptions.useCache];
}
return this._dispatchEvent(eventName, inputArgs);
if (options?.debounce) {
return this._dispatchEventWithDebounce(eventName, inputArgs, execOptions);
}
return this._dispatchEvent(eventName, inputArgs, execOptions);
}
/**
* 获取所有自动应用流程定义(保持 getFlows 的顺序,即按 sort 排序)
* @returns {FlowDefinition[]} 自动应用流程定义数组(已按 sort 排序)
* 按事件名获取对应的流程集合(保持 getFlows 的顺序,即按 sort 排序)。
* - beforeRender 兼容:除显式 on: 'beforeRender' 外,包含未声明 on 且 manual !== true 的流程。
*/
public getAutoFlows(): FlowDefinition[] {
public getEventFlows(eventName: string): FlowDefinition[] {
const allFlows = this.getFlows();
// 过滤出自动流程(保持 Map 的原有顺序)
// 没有 on 属性且没有 manual: true 的流程默认自动执行
const autoFlows = Array.from(allFlows.values()).filter((flow) => {
// 如果有 on 属性,说明是事件触发流程,不自动执行
if (flow.on) {
return false;
const beforeRender = eventName === 'beforeRender';
const isMatch = (flow: FlowDefinition) => {
if (beforeRender) {
if (flow.manual === true) return false;
if (!flow.on) return true;
return typeof flow.on === 'string' ? flow.on === 'beforeRender' : flow.on?.eventName === 'beforeRender';
}
// 如果明确设置了 manual: true,不自动执行
if (flow.manual === true) {
return false;
}
// 其他情况默认自动执行
return true;
});
return autoFlows;
const on = flow.on;
if (!on) return false;
return typeof on === 'string' ? on === eventName : on?.eventName === eventName;
};
return Array.from(allFlows.values()).filter(isMatch);
}
/**
* 重新执行上一次的 applyAutoFlows,保持参数一致
* 重新执行上一次的 beforeRender,保持参数一致
* 如果之前没有执行过,则直接跳过
* 使用 lodash debounce 避免频繁调用
*/
private _rerunLastAutoRun = _.debounce(async () => {
if (this._lastAutoRunParams) {
try {
await this.applyAutoFlows(...this._lastAutoRunParams);
const [inputArgs] = this._lastAutoRunParams as any[];
await this.dispatchEvent('beforeRender', inputArgs);
} catch (error) {
console.error('FlowModel._rerunLastAutoRun: Error during rerun:', error);
}
@@ -715,86 +778,39 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
}, 100);
/**
* 自动流程执行前钩子。
* 子类可覆盖;可抛出 FlowExitException 提前终止。
* 通用事件分发钩子:开始
* 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
*/
public async onBeforeAutoFlows(inputArgs?: Record<string, any>): Promise<void> {}
public async onDispatchEventStart(
eventName: string,
options?: DispatchEventOptions,
inputArgs?: Record<string, any>,
): Promise<void> {}
/**
* 自动流程执行后钩子。
* 通用事件分发钩子:结束
* 子类可覆盖。
*/
public async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>): Promise<void> {}
public async onDispatchEventEnd(
eventName: string,
options?: DispatchEventOptions,
inputArgs?: Record<string, any>,
results?: any[],
): Promise<void> {}
/**
* 自动流程错误钩子。
* 通用事件分发钩子:错误
* 子类可覆盖。
*/
public async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>): Promise<void> {}
public async onDispatchEventError(
eventName: string,
options?: DispatchEventOptions,
inputArgs?: Record<string, any>,
error?: Error,
): Promise<void> {}
useHooksBeforeRender() {}
/**
* 执行所有自动应用流程
* @param {Record<string, any>} [inputArgs] 可选的运行时参数
* @param {boolean} [useCache=true] 是否使用缓存机制,默认为 true
* @returns {Promise<any[]>} 所有自动应用流程的执行结果数组
*/
async applyAutoFlows(inputArgs?: Record<string, any>, useCache?: boolean): Promise<any[]>;
async applyAutoFlows(...args: any[]): Promise<any[]> {
const [inputArgs, useCache = true] = args;
const cacheKey = useCache
? FlowEngine.generateApplyFlowCacheKey(this['forkId'] ?? 'autoFlow', 'all', this.uid)
: null;
if (!_.isEqual(inputArgs, this._lastAutoRunParams?.[0]) && cacheKey) {
this.flowEngine.applyFlowCache.delete(cacheKey);
}
this._lastAutoRunParams = args;
// 在执行自动流程前触发(无论是否命中缓存都应触发)
try {
await this.onBeforeAutoFlows(inputArgs);
} catch (error) {
if (error instanceof FlowExitException) {
this.context.logger?.debug(`[FlowModel.applyAutoFlows] ${error.message}`);
return [];
}
try {
await this.onAutoFlowsError(error as Error, inputArgs);
} catch (_) {
// swallow secondary hook error to avoid masking the original
}
throw error;
}
// 执行自动流程(内部可能命中缓存)
let results: any[] = [];
try {
results = await this.flowEngine.executor.runAutoFlows(this, inputArgs, useCache);
} catch (error) {
try {
await this.onAutoFlowsError(error as Error, inputArgs);
} catch (_) {
// ignore secondary error from error hook
}
throw error;
}
// 执行后钩子
try {
await this.onAfterAutoFlows(results, inputArgs);
} catch (error) {
try {
await this.onAutoFlowsError(error as Error, inputArgs);
} catch (_) {
// ignore
}
throw error;
}
return results;
}
/**
* 智能检测是否应该跳过响应式包装
* 说明:
@@ -891,6 +907,10 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
return modelInstance.renderHiddenInConfig?.();
}
// 订阅 stepParams 变化,以便当步骤参数(如 JS 代码等)更新时触发一次 React 渲染,
// 从而让 useHooksBeforeRender 中的副作用得以感知并执行(如 applyFlow('jsSettings'))。
modelInstance.stepParams;
// 调用原始渲染方法
return originalRender.call(renderTarget);
});
@@ -962,14 +982,14 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
}
async rerender() {
await this.applyAutoFlows(this._lastAutoRunParams?.[0], false);
await this.dispatchEvent('beforeRender', this._lastAutoRunParams?.[0], { useCache: false });
}
/**
* 自动流程缓存的作用域标识;fork 实例可覆盖以区分缓存。
* 事件缓存的作用域标识;可按事件区分(默认与事件无关的 scope 返回 'default')。
*/
public getAutoFlowCacheScope(): string {
return 'autoFlow';
public getFlowCacheScope(eventName: string): string {
return String(eventName);
}
setParent(parent: FlowModel): void {
@@ -1128,14 +1148,17 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
return this.flowEngine.createModel(options);
}
async applySubModelsAutoFlows<K extends keyof Structure['subModels']>(
/**
* 对指定子模型派发 beforeRender 事件(顺序执行并使用缓存)。
*/
async applySubModelsBeforeRenderFlows<K extends keyof Structure['subModels']>(
subKey: K,
inputArgs?: Record<string, any>,
shared?: Record<string, any>,
) {
await Promise.all(
this.mapSubModels(subKey, async (sub) => {
await sub.applyAutoFlows(inputArgs);
await sub.dispatchEvent('beforeRender', inputArgs);
}),
);
}
@@ -1209,7 +1232,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
throw new Error('FlowEngine is not set on this model. Please set flowEngine before saving.');
}
this.observerDispose();
this.invalidateAutoFlowCache(true);
this.invalidateFlowCache('beforeRender', true);
return this.flowEngine.removeModel(this.uid);
}
@@ -1229,7 +1252,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
throw new Error('FlowEngine is not set on this model. Please set flowEngine before deleting.');
}
this.observerDispose();
this.invalidateAutoFlowCache(true);
this.invalidateFlowCache('beforeRender', true);
// 从 FlowEngine 中销毁模型
return this.flowEngine.destroyModel(this.uid);
}
@@ -224,10 +224,10 @@ export class ForkFlowModel<TMaster extends FlowModel = FlowModel> {
}
/**
* 自动流程缓存的作用域标识(fork 专用)。
* 事件缓存的作用域标识(fork 专用)。
*/
public getAutoFlowCacheScope(): string {
return String(this.forkId);
public getFlowCacheScope(eventName: string): string {
return `${String(eventName)}:${String(this.forkId)}`;
}
// onUnmount() {
@@ -250,9 +250,41 @@ export class ForkFlowModel<TMaster extends FlowModel = FlowModel> {
dispose() {
if (this.disposed) return;
this.disposed = true;
// 统一按事件缓存命名规则清理当前 fork 的所有缓存项(覆盖所有事件)
try {
const cache = this.flowEngine?.applyFlowCache;
if (cache && cache.size > 0) {
const uidSuffixes = [`:${this.uid}`, `-${this.uid}`];
const forkIdColon = `:${this.forkId}:`;
const forkIdHyphen = `-${this.forkId}-`;
for (const key of cache.keys()) {
// 事件缓存统一以 event: 或 event- 开头
const isEventCache = key.startsWith('event:') || key.startsWith('event-');
if (!isEventCache) continue;
// 仅删除当前 fork + 当前 uid 的键
const endMatches = uidSuffixes.some((s) => key.endsWith(s));
if (!endMatches) continue;
// 限定在去除 uid 后缀的范围内匹配 forkId 片段,避免误伤
let trimmed = key;
for (const suf of uidSuffixes) {
if (trimmed.endsWith(suf)) {
trimmed = trimmed.slice(0, -suf.length);
break;
}
}
if (trimmed.includes(forkIdColon) || trimmed.includes(forkIdHyphen)) {
cache.delete(key);
}
}
}
} catch {
// 忽略清理阶段异常,避免影响后续释放
}
if (this.master && (this.master as any).forks) {
const forkCacheKey = FlowEngine.generateApplyFlowCacheKey(`${this.forkId}`, 'all', this.uid);
this.flowEngine.applyFlowCache.delete(forkCacheKey);
(this.master as any).forks.delete(this as any);
}
// 从 master 的 forkCache 中移除自己
+10
View File
@@ -185,6 +185,16 @@ export type FlowEvent<TModel extends FlowModel = FlowModel> =
| FlowEventName
| { eventName: FlowEventName; defaultParams?: Record<string, any> };
/**
* 事件分发选项。
*/
export interface DispatchEventOptions {
/** 是否顺序执行(默认并行) */
sequential?: boolean;
/** 是否使用缓存(默认 false;beforeRender 的默认值为 true,可覆盖) */
useCache?: boolean;
}
/**
* 事件定义:用于事件注册表(全局/模型类级)。
*/
@@ -32,7 +32,7 @@ export class JavaScriptBlockModel extends BlockModel {
}
protected onMount() {
// If component has mounted before, rerun to refresh JS on remount
// Keep first mount single-run (handled by auto-flows + onRefReady)
// Keep first mount single-run (handled by beforeRender event + onRefReady)
if (this._mountedOnce) {
if (this.context.ref.current) {
this.rerender();
@@ -228,7 +228,7 @@ export class ChartBlockModel extends DataBlockModel<ChartBlockModelStructure> {
}
this.setStepParams('chartSettings', 'configure', params);
this.rerender();
// 调用链:rerender -> applyAutoFlows -> handler(处理数据、属性、事件) -> runQueryAndUpdateResult(查数据)
// 调用链:rerender -> dispatchEvent('beforeRender') -> handler(处理数据、属性、事件) -> runQueryAndUpdateResult(查数据)
}
// 取消预览,回滚stepParams,并刷新图表