@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -661
- package/README.md +79 -10
- package/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +6 -0
- package/lib/FlowSchemaRegistry.d.ts +154 -0
- package/lib/FlowSchemaRegistry.js +1427 -0
- package/lib/JSRunner.d.ts +15 -0
- package/lib/JSRunner.js +82 -7
- package/lib/ViewScopedFlowEngine.js +8 -1
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
- package/lib/components/dnd/gridDragPlanner.js +59 -3
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
- package/lib/components/subModel/AddSubModelButton.js +27 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +84 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +190 -26
- package/lib/flow-schema-registry/fieldBinding.d.ts +32 -0
- package/lib/flow-schema-registry/fieldBinding.js +165 -0
- package/lib/flow-schema-registry/modelPatches.d.ts +16 -0
- package/lib/flow-schema-registry/modelPatches.js +235 -0
- package/lib/flow-schema-registry/schemaInference.d.ts +17 -0
- package/lib/flow-schema-registry/schemaInference.js +207 -0
- package/lib/flow-schema-registry/utils.d.ts +25 -0
- package/lib/flow-schema-registry/utils.js +293 -0
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2270 -148
- package/lib/flowEngine.d.ts +160 -1
- package/lib/flowEngine.js +387 -27
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +51 -17
- package/lib/index.d.ts +8 -1
- package/lib/index.js +24 -1
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +7 -0
- package/lib/models/flowModel.js +83 -8
- package/lib/provider.js +7 -6
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/registry.d.ts +1 -1
- package/lib/runjs-context/setup.js +23 -9
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
- package/lib/scheduler/ModelOperationScheduler.js +28 -23
- package/lib/server.d.ts +10 -0
- package/lib/server.js +32 -0
- package/lib/types.d.ts +296 -1
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +49 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/parsePathnameToViewParams.js +1 -1
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +17 -1
- package/lib/utils/schema-utils.js +80 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +28 -6
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +27 -5
- package/lib/views/usePage.d.ts +6 -1
- package/lib/views/usePage.js +53 -9
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +5 -5
- package/server.d.ts +1 -0
- package/server.js +1 -0
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/FlowSchemaRegistry.ts +1799 -0
- package/src/JSRunner.ts +111 -5
- package/src/ViewScopedFlowEngine.ts +8 -0
- package/src/__tests__/FlowSchemaRegistry.test.ts +1951 -0
- package/src/__tests__/JSRunner.test.ts +91 -1
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flow-engine.test.ts +48 -0
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowEngine.modelLoaders.test.ts +249 -0
- package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/provider.test.tsx +0 -5
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +26 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +5 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
- package/src/components/dnd/gridDragPlanner.ts +68 -2
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/subModel/AddSubModelButton.tsx +32 -2
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +143 -32
- package/src/components/subModel/utils.ts +1 -1
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +88 -110
- package/src/executor/FlowExecutor.ts +230 -28
- package/src/executor/__tests__/flowExecutor.test.ts +123 -0
- package/src/flow-schema-registry/fieldBinding.ts +171 -0
- package/src/flow-schema-registry/modelPatches.ts +260 -0
- package/src/flow-schema-registry/schemaInference.ts +210 -0
- package/src/flow-schema-registry/utils.ts +268 -0
- package/src/flowContext.ts +2989 -212
- package/src/flowEngine.ts +434 -23
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +58 -18
- package/src/index.ts +15 -1
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +768 -0
- package/src/models/__tests__/flowModel.clone.test.ts +416 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +112 -7
- package/src/provider.tsx +9 -7
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/registry.ts +1 -1
- package/src/runjs-context/setup.ts +25 -9
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +41 -24
- package/src/server.ts +11 -0
- package/src/types.ts +359 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +157 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +38 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/parsePathnameToViewParams.ts +2 -2
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +109 -1
- package/src/views/FlowView.tsx +11 -1
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +34 -5
- package/src/views/useDrawer.tsx +33 -4
- package/src/views/usePage.tsx +63 -8
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -17,10 +17,20 @@ import { FlowExitException, resolveDefaultParams } from '../utils';
|
|
|
17
17
|
import { FlowExitAllException } from '../utils/exceptions';
|
|
18
18
|
import { setupRuntimeContextSteps } from '../utils/setupRuntimeContextSteps';
|
|
19
19
|
import { createEphemeralContext } from '../utils/createEphemeralContext';
|
|
20
|
+
import type { ScheduledCancel } from '../scheduler/ModelOperationScheduler';
|
|
20
21
|
|
|
21
22
|
export class FlowExecutor {
|
|
22
23
|
constructor(private readonly engine: FlowEngine) {}
|
|
23
24
|
|
|
25
|
+
private async emitModelEventIf(
|
|
26
|
+
eventName: string | undefined,
|
|
27
|
+
topic: string,
|
|
28
|
+
payload: Record<string, any>,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (!eventName) return;
|
|
31
|
+
await this.engine.emitter.emitAsync(`model:event:${eventName}:${topic}`, payload);
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
/** Cache wrapper for applyFlow cache lifecycle */
|
|
25
35
|
private async withApplyFlowCache<T>(cacheKey: string | null, executor: () => Promise<T>): Promise<T> {
|
|
26
36
|
if (!cacheKey || !this.engine) return await executor();
|
|
@@ -57,7 +67,13 @@ export class FlowExecutor {
|
|
|
57
67
|
/**
|
|
58
68
|
* Execute a single flow on model.
|
|
59
69
|
*/
|
|
60
|
-
async runFlow(
|
|
70
|
+
async runFlow(
|
|
71
|
+
model: FlowModel,
|
|
72
|
+
flowKey: string,
|
|
73
|
+
inputArgs?: Record<string, any>,
|
|
74
|
+
runId?: string,
|
|
75
|
+
eventName?: string,
|
|
76
|
+
): Promise<any> {
|
|
61
77
|
const flow = model.getFlow(flowKey);
|
|
62
78
|
|
|
63
79
|
if (!flow) {
|
|
@@ -99,6 +115,16 @@ export class FlowExecutor {
|
|
|
99
115
|
setupRuntimeContextSteps(flowContext, stepDefs, model, flowKey);
|
|
100
116
|
const stepsRuntime = flowContext.steps as Record<string, { params: any; uiSchema?: any; result?: any }>;
|
|
101
117
|
|
|
118
|
+
const flowEventBasePayload = {
|
|
119
|
+
uid: model.uid,
|
|
120
|
+
model,
|
|
121
|
+
runId: flowContext.runId,
|
|
122
|
+
inputArgs,
|
|
123
|
+
flowKey,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:start`, flowEventBasePayload);
|
|
127
|
+
|
|
102
128
|
for (const [stepKey, step] of Object.entries(stepDefs) as [string, StepDefinition][]) {
|
|
103
129
|
// Resolve handler and params
|
|
104
130
|
let handler: ActionDefinition<FlowModel, FlowRuntimeContext>['handler'] | undefined;
|
|
@@ -156,6 +182,11 @@ export class FlowExecutor {
|
|
|
156
182
|
);
|
|
157
183
|
continue;
|
|
158
184
|
}
|
|
185
|
+
|
|
186
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:step:${stepKey}:start`, {
|
|
187
|
+
...flowEventBasePayload,
|
|
188
|
+
stepKey,
|
|
189
|
+
});
|
|
159
190
|
const currentStepResult = handler(runtimeCtx, combinedParams);
|
|
160
191
|
const isAwait = step.isAwait !== false;
|
|
161
192
|
lastResult = isAwait ? await currentStepResult : currentStepResult;
|
|
@@ -163,15 +194,49 @@ export class FlowExecutor {
|
|
|
163
194
|
// Store step result and update context
|
|
164
195
|
stepResults[stepKey] = lastResult;
|
|
165
196
|
stepsRuntime[stepKey].result = stepResults[stepKey];
|
|
197
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:step:${stepKey}:end`, {
|
|
198
|
+
...flowEventBasePayload,
|
|
199
|
+
result: lastResult,
|
|
200
|
+
stepKey,
|
|
201
|
+
});
|
|
166
202
|
} catch (error) {
|
|
203
|
+
if (!(error instanceof FlowExitException) && !(error instanceof FlowExitAllException)) {
|
|
204
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:step:${stepKey}:error`, {
|
|
205
|
+
...flowEventBasePayload,
|
|
206
|
+
error,
|
|
207
|
+
stepKey,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
167
210
|
if (error instanceof FlowExitException) {
|
|
168
211
|
flowContext.logger.info(`[FlowEngine] ${error.message}`);
|
|
212
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:step:${stepKey}:end`, {
|
|
213
|
+
...flowEventBasePayload,
|
|
214
|
+
stepKey,
|
|
215
|
+
});
|
|
216
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:end`, {
|
|
217
|
+
...flowEventBasePayload,
|
|
218
|
+
result: stepResults,
|
|
219
|
+
});
|
|
169
220
|
return Promise.resolve(stepResults);
|
|
170
221
|
}
|
|
171
222
|
if (error instanceof FlowExitAllException) {
|
|
172
223
|
flowContext.logger.info(`[FlowEngine] ${error.message}`);
|
|
224
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:step:${stepKey}:end`, {
|
|
225
|
+
...flowEventBasePayload,
|
|
226
|
+
stepKey,
|
|
227
|
+
aborted: true,
|
|
228
|
+
});
|
|
229
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:end`, {
|
|
230
|
+
...flowEventBasePayload,
|
|
231
|
+
result: error,
|
|
232
|
+
aborted: true,
|
|
233
|
+
});
|
|
173
234
|
return Promise.resolve(error);
|
|
174
235
|
}
|
|
236
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:error`, {
|
|
237
|
+
...flowEventBasePayload,
|
|
238
|
+
error,
|
|
239
|
+
});
|
|
175
240
|
flowContext.logger.error(
|
|
176
241
|
{ err: error },
|
|
177
242
|
`BaseModel.applyFlow: Error executing step '${stepKey}' in flow '${flowKey}':`,
|
|
@@ -179,11 +244,13 @@ export class FlowExecutor {
|
|
|
179
244
|
return Promise.reject(error);
|
|
180
245
|
}
|
|
181
246
|
}
|
|
247
|
+
await this.emitModelEventIf(eventName, `flow:${flowKey}:end`, {
|
|
248
|
+
...flowEventBasePayload,
|
|
249
|
+
result: stepResults,
|
|
250
|
+
});
|
|
182
251
|
return Promise.resolve(stepResults);
|
|
183
252
|
}
|
|
184
253
|
|
|
185
|
-
// runAutoFlows 已移除:统一通过 dispatchEvent('beforeRender') + useCache 控制
|
|
186
|
-
|
|
187
254
|
/**
|
|
188
255
|
* Dispatch an event to flows bound via flow.on and execute them.
|
|
189
256
|
*/
|
|
@@ -202,14 +269,15 @@ export class FlowExecutor {
|
|
|
202
269
|
|
|
203
270
|
const runId = `${model.uid}-${eventName}-${Date.now()}`;
|
|
204
271
|
const logger = model.context.logger;
|
|
272
|
+
const eventBasePayload = {
|
|
273
|
+
uid: model.uid,
|
|
274
|
+
model,
|
|
275
|
+
runId,
|
|
276
|
+
inputArgs,
|
|
277
|
+
};
|
|
205
278
|
|
|
206
279
|
try {
|
|
207
|
-
await this.
|
|
208
|
-
uid: model.uid,
|
|
209
|
-
model,
|
|
210
|
-
runId,
|
|
211
|
-
inputArgs,
|
|
212
|
-
});
|
|
280
|
+
await this.emitModelEventIf(eventName, 'start', eventBasePayload);
|
|
213
281
|
await model.onDispatchEventStart?.(eventName, options, inputArgs);
|
|
214
282
|
} catch (err) {
|
|
215
283
|
if (isBeforeRender && err instanceof FlowExitException) {
|
|
@@ -237,11 +305,25 @@ export class FlowExecutor {
|
|
|
237
305
|
return false;
|
|
238
306
|
});
|
|
239
307
|
|
|
240
|
-
//
|
|
241
|
-
|
|
308
|
+
// 路由系统的“重放打开视图”会再次 dispatchEvent('click'),但这不应重复触发用户配置的动态事件流。
|
|
309
|
+
// 约定:由路由重放触发时,会在 inputArgs 中携带 triggerByRouter: true
|
|
310
|
+
const isRouterReplayClick = eventName === 'click' && inputArgs?.triggerByRouter === true;
|
|
311
|
+
const flowsToRun = isRouterReplayClick
|
|
312
|
+
? flows.filter((flow) => {
|
|
313
|
+
const reg = flow['flowRegistry'] as any;
|
|
314
|
+
const type = reg?.constructor?._type as 'instance' | 'global' | undefined;
|
|
315
|
+
return type !== 'instance';
|
|
316
|
+
})
|
|
317
|
+
: flows;
|
|
318
|
+
|
|
319
|
+
// 记录本次 dispatchEvent 内注册的调度任务,用于在结束/错误后兜底清理未触发的任务
|
|
320
|
+
const scheduledCancels: ScheduledCancel[] = [];
|
|
321
|
+
// 组装执行函数(返回值用于缓存,包含事件结果与是否被 exitAll 中止)
|
|
322
|
+
const execute = async (): Promise<{ result: any[]; abortedByExitAll: boolean }> => {
|
|
323
|
+
let abortedByExitAll = false;
|
|
242
324
|
if (sequential) {
|
|
243
325
|
// 顺序执行:动态流(实例级)优先,其次静态流;各自组内再按 sort 升序,最后保持原始顺序稳定
|
|
244
|
-
const flowsWithIndex =
|
|
326
|
+
const flowsWithIndex = flowsToRun.map((f, i) => ({ f, i }));
|
|
245
327
|
const ordered = flowsWithIndex
|
|
246
328
|
.slice()
|
|
247
329
|
.sort((a, b) => {
|
|
@@ -259,18 +341,128 @@ export class FlowExecutor {
|
|
|
259
341
|
})
|
|
260
342
|
.map((x) => x.f);
|
|
261
343
|
const results: any[] = [];
|
|
344
|
+
|
|
345
|
+
// 预处理:当事件流配置了 on.phase 时,将其执行移动到指定节点,并从“立即执行列表”中移除
|
|
346
|
+
const staticFlowsByKey = new Map(
|
|
347
|
+
ordered
|
|
348
|
+
.filter((f) => {
|
|
349
|
+
const reg = f['flowRegistry'] as any;
|
|
350
|
+
const type = reg?.constructor?._type as 'instance' | 'global' | undefined;
|
|
351
|
+
return type !== 'instance';
|
|
352
|
+
})
|
|
353
|
+
.map((f) => [f.key, f] as const),
|
|
354
|
+
);
|
|
355
|
+
const scheduled = new Set<string>();
|
|
356
|
+
const scheduleGroups = new Map<string, Array<{ flow: any; order: number; shouldSkipOnAborted: boolean }>>();
|
|
357
|
+
ordered.forEach((flow, indexInOrdered) => {
|
|
358
|
+
const on = flow.on;
|
|
359
|
+
const onObj = typeof on === 'object' ? (on as any) : undefined;
|
|
360
|
+
if (!onObj) return;
|
|
361
|
+
|
|
362
|
+
const phase: any = onObj.phase;
|
|
363
|
+
const flowKey: any = onObj.flowKey;
|
|
364
|
+
const stepKey: any = onObj.stepKey;
|
|
365
|
+
|
|
366
|
+
// 默认:beforeAllFlows(保持现有行为)
|
|
367
|
+
if (!phase || phase === 'beforeAllFlows') return;
|
|
368
|
+
|
|
369
|
+
let whenKey: string | null = null;
|
|
370
|
+
if (phase === 'afterAllFlows') {
|
|
371
|
+
whenKey = `event:${eventName}:end`;
|
|
372
|
+
} else if (phase === 'beforeFlow' || phase === 'afterFlow') {
|
|
373
|
+
if (!flowKey) {
|
|
374
|
+
// 配置不完整:降级到“全部静态流之后”
|
|
375
|
+
whenKey = `event:${eventName}:end`;
|
|
376
|
+
} else {
|
|
377
|
+
const anchorFlow = staticFlowsByKey.get(String(flowKey));
|
|
378
|
+
if (anchorFlow) {
|
|
379
|
+
const anchorPhase = phase === 'beforeFlow' ? 'start' : 'end';
|
|
380
|
+
whenKey = `event:${eventName}:flow:${String(flowKey)}:${anchorPhase}`;
|
|
381
|
+
} else {
|
|
382
|
+
// 锚点不存在(flow 被删除或覆盖等):降级到“全部静态流之后”
|
|
383
|
+
whenKey = `event:${eventName}:end`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else if (phase === 'beforeStep' || phase === 'afterStep') {
|
|
387
|
+
if (!flowKey || !stepKey) {
|
|
388
|
+
// 配置不完整:降级到“全部静态流之后”
|
|
389
|
+
whenKey = `event:${eventName}:end`;
|
|
390
|
+
} else {
|
|
391
|
+
const anchorFlow = staticFlowsByKey.get(String(flowKey));
|
|
392
|
+
const anchorStepExists = !!anchorFlow?.hasStep?.(String(stepKey));
|
|
393
|
+
if (anchorFlow && anchorStepExists) {
|
|
394
|
+
const anchorPhase = phase === 'beforeStep' ? 'start' : 'end';
|
|
395
|
+
whenKey = `event:${eventName}:flow:${String(flowKey)}:step:${String(stepKey)}:${anchorPhase}`;
|
|
396
|
+
} else {
|
|
397
|
+
// 锚点不存在(flow/step 被删除或覆盖等):降级到“全部静态流之后”
|
|
398
|
+
whenKey = `event:${eventName}:end`;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// 未知 phase:忽略
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!whenKey) return;
|
|
407
|
+
const shouldSkipOnAborted =
|
|
408
|
+
whenKey === `event:${eventName}:end` || phase === 'afterFlow' || phase === 'afterStep';
|
|
409
|
+
scheduled.add(flow.key);
|
|
410
|
+
const list = scheduleGroups.get(whenKey) || [];
|
|
411
|
+
list.push({ flow, order: indexInOrdered, shouldSkipOnAborted });
|
|
412
|
+
scheduleGroups.set(whenKey, list);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// 注册调度(同锚点按 flow.sort 升序;sort 相同保持稳定顺序)
|
|
416
|
+
for (const [whenKey, list] of scheduleGroups.entries()) {
|
|
417
|
+
const sorted = list.slice().sort((a, b) => {
|
|
418
|
+
const sa = a.flow.sort ?? 0;
|
|
419
|
+
const sb = b.flow.sort ?? 0;
|
|
420
|
+
if (sa !== sb) return sa - sb;
|
|
421
|
+
return a.order - b.order;
|
|
422
|
+
});
|
|
423
|
+
for (const it of sorted) {
|
|
424
|
+
const when = it.shouldSkipOnAborted
|
|
425
|
+
? Object.assign(
|
|
426
|
+
(event: { type: string; aborted?: boolean }) => event.type === whenKey && event.aborted !== true,
|
|
427
|
+
{
|
|
428
|
+
__eventType: whenKey,
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
: (whenKey as any);
|
|
432
|
+
const cancel = model.scheduleModelOperation(
|
|
433
|
+
model.uid,
|
|
434
|
+
async (m) => {
|
|
435
|
+
const res = await this.runFlow(m, it.flow.key, inputArgs, runId, eventName);
|
|
436
|
+
if (res instanceof FlowExitAllException) {
|
|
437
|
+
throw res;
|
|
438
|
+
}
|
|
439
|
+
results.push(res);
|
|
440
|
+
},
|
|
441
|
+
{ when },
|
|
442
|
+
);
|
|
443
|
+
scheduledCancels.push(cancel);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
262
447
|
for (const flow of ordered) {
|
|
448
|
+
if (scheduled.has(flow.key)) continue;
|
|
263
449
|
try {
|
|
264
450
|
logger.debug(
|
|
265
451
|
`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}' (sequential).`,
|
|
266
452
|
);
|
|
267
|
-
const result = await this.runFlow(model, flow.key, inputArgs, runId);
|
|
453
|
+
const result = await this.runFlow(model, flow.key, inputArgs, runId, eventName);
|
|
268
454
|
if (result instanceof FlowExitAllException) {
|
|
269
455
|
logger.debug(`[FlowEngine.dispatchEvent] ${result.message}`);
|
|
456
|
+
abortedByExitAll = true;
|
|
270
457
|
break; // 终止后续
|
|
271
458
|
}
|
|
272
459
|
results.push(result);
|
|
273
460
|
} catch (error) {
|
|
461
|
+
if (error instanceof FlowExitAllException) {
|
|
462
|
+
logger.debug(`[FlowEngine.dispatchEvent] ${error.message}`);
|
|
463
|
+
abortedByExitAll = true;
|
|
464
|
+
break; // 终止后续
|
|
465
|
+
}
|
|
274
466
|
logger.error(
|
|
275
467
|
{ err: error },
|
|
276
468
|
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}' (sequential):`,
|
|
@@ -278,15 +470,15 @@ export class FlowExecutor {
|
|
|
278
470
|
throw error;
|
|
279
471
|
}
|
|
280
472
|
}
|
|
281
|
-
return results;
|
|
473
|
+
return { result: results, abortedByExitAll };
|
|
282
474
|
}
|
|
283
475
|
|
|
284
476
|
// 并行
|
|
285
477
|
const results = await Promise.all(
|
|
286
|
-
|
|
478
|
+
flowsToRun.map(async (flow) => {
|
|
287
479
|
logger.debug(`BaseModel '${model.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`);
|
|
288
480
|
try {
|
|
289
|
-
return await this.runFlow(model, flow.key, inputArgs, runId);
|
|
481
|
+
return await this.runFlow(model, flow.key, inputArgs, runId, eventName);
|
|
290
482
|
} catch (error) {
|
|
291
483
|
logger.error(
|
|
292
484
|
{ err: error },
|
|
@@ -297,7 +489,11 @@ export class FlowExecutor {
|
|
|
297
489
|
}
|
|
298
490
|
}),
|
|
299
491
|
);
|
|
300
|
-
|
|
492
|
+
const filteredResults = results.filter((x) => x !== undefined);
|
|
493
|
+
if (filteredResults.some((x) => x instanceof FlowExitAllException)) {
|
|
494
|
+
abortedByExitAll = true;
|
|
495
|
+
}
|
|
496
|
+
return { result: filteredResults, abortedByExitAll };
|
|
301
497
|
};
|
|
302
498
|
|
|
303
499
|
// 缓存键:按事件+scope 统一管理(beforeRender 也使用事件名 beforeRender)
|
|
@@ -311,20 +507,24 @@ export class FlowExecutor {
|
|
|
311
507
|
: null;
|
|
312
508
|
|
|
313
509
|
try {
|
|
314
|
-
const result = await this.withApplyFlowCache(cacheKey, execute);
|
|
510
|
+
const { result, abortedByExitAll } = await this.withApplyFlowCache(cacheKey, execute);
|
|
315
511
|
// 事件结束钩子
|
|
316
512
|
try {
|
|
317
513
|
await model.onDispatchEventEnd?.(eventName, options, inputArgs, result);
|
|
318
514
|
} catch (hookErr) {
|
|
319
515
|
logger.error({ err: hookErr }, `BaseModel.dispatchEvent: End hook error for event '${eventName}'`);
|
|
320
516
|
}
|
|
321
|
-
await this.
|
|
322
|
-
|
|
323
|
-
model,
|
|
324
|
-
runId,
|
|
325
|
-
inputArgs,
|
|
517
|
+
await this.emitModelEventIf(eventName, 'end', {
|
|
518
|
+
...eventBasePayload,
|
|
326
519
|
result,
|
|
520
|
+
...(abortedByExitAll ? { aborted: true } : {}),
|
|
327
521
|
});
|
|
522
|
+
if (result && typeof result === 'object') {
|
|
523
|
+
Object.defineProperty(result, '__abortedByExitAll', {
|
|
524
|
+
value: abortedByExitAll,
|
|
525
|
+
configurable: true,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
328
528
|
return result;
|
|
329
529
|
} catch (error) {
|
|
330
530
|
// 进入错误钩子并记录
|
|
@@ -337,14 +537,16 @@ export class FlowExecutor {
|
|
|
337
537
|
{ err: error },
|
|
338
538
|
`BaseModel.dispatchEvent: Error executing event '${eventName}' for model '${model.uid}':`,
|
|
339
539
|
);
|
|
340
|
-
await this.
|
|
341
|
-
|
|
342
|
-
model,
|
|
343
|
-
runId,
|
|
344
|
-
inputArgs,
|
|
540
|
+
await this.emitModelEventIf(eventName, 'error', {
|
|
541
|
+
...eventBasePayload,
|
|
345
542
|
error,
|
|
346
543
|
});
|
|
347
544
|
if (throwOnError) throw error;
|
|
545
|
+
} finally {
|
|
546
|
+
// 清理未触发的调度任务,避免跨事件/跨 runId 残留导致意外执行
|
|
547
|
+
for (const cancel of scheduledCancels) {
|
|
548
|
+
cancel();
|
|
549
|
+
}
|
|
348
550
|
}
|
|
349
551
|
}
|
|
350
552
|
}
|
|
@@ -139,6 +139,72 @@ describe('FlowExecutor', () => {
|
|
|
139
139
|
expect(submitHandler).not.toHaveBeenCalled();
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("dispatchEvent('click') skips instance flows when triggerByRouter is true", async () => {
|
|
143
|
+
class MyModel extends FlowModel {}
|
|
144
|
+
|
|
145
|
+
const globalHandler = vi.fn().mockResolvedValue('global-ok');
|
|
146
|
+
MyModel.registerFlow('globalClick', {
|
|
147
|
+
on: 'click',
|
|
148
|
+
steps: {
|
|
149
|
+
s: { handler: globalHandler },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const instanceHandler = vi.fn().mockResolvedValue('instance-ok');
|
|
154
|
+
const model = new MyModel({
|
|
155
|
+
uid: 'm-click-router-replay',
|
|
156
|
+
flowEngine: engine,
|
|
157
|
+
flowRegistry: {
|
|
158
|
+
instanceClick: {
|
|
159
|
+
on: 'click',
|
|
160
|
+
steps: {
|
|
161
|
+
s: { handler: instanceHandler },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
stepParams: {},
|
|
166
|
+
subModels: {},
|
|
167
|
+
} as FlowModelOptions);
|
|
168
|
+
|
|
169
|
+
await engine.executor.dispatchEvent(model, 'click', { triggerByRouter: true }, { sequential: true });
|
|
170
|
+
|
|
171
|
+
expect(globalHandler).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(instanceHandler).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("dispatchEvent('click') keeps instance flows when triggerByRouter is not true", async () => {
|
|
176
|
+
class MyModel extends FlowModel {}
|
|
177
|
+
|
|
178
|
+
const globalHandler = vi.fn().mockResolvedValue('global-ok');
|
|
179
|
+
MyModel.registerFlow('globalClick', {
|
|
180
|
+
on: 'click',
|
|
181
|
+
steps: {
|
|
182
|
+
s: { handler: globalHandler },
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const instanceHandler = vi.fn().mockResolvedValue('instance-ok');
|
|
187
|
+
const model = new MyModel({
|
|
188
|
+
uid: 'm-click-normal',
|
|
189
|
+
flowEngine: engine,
|
|
190
|
+
flowRegistry: {
|
|
191
|
+
instanceClick: {
|
|
192
|
+
on: 'click',
|
|
193
|
+
steps: {
|
|
194
|
+
s: { handler: instanceHandler },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
stepParams: {},
|
|
199
|
+
subModels: {},
|
|
200
|
+
} as FlowModelOptions);
|
|
201
|
+
|
|
202
|
+
await engine.executor.dispatchEvent(model, 'click', { triggerByRouter: false }, { sequential: true });
|
|
203
|
+
|
|
204
|
+
expect(globalHandler).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect(instanceHandler).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
142
208
|
it('dispatchEvent default parallel does not stop on exitAll', async () => {
|
|
143
209
|
const calls: string[] = [];
|
|
144
210
|
const mkFlow = (key: string, opts?: { exitAll?: boolean }) => ({
|
|
@@ -166,6 +232,37 @@ describe('FlowExecutor', () => {
|
|
|
166
232
|
expect(calls.sort()).toEqual(['a', 'b']);
|
|
167
233
|
});
|
|
168
234
|
|
|
235
|
+
it('dispatchEvent sequential exposes abortedByExitAll metadata on result array', async () => {
|
|
236
|
+
const flows = {
|
|
237
|
+
stopClose: {
|
|
238
|
+
on: { eventName: 'close' },
|
|
239
|
+
steps: {
|
|
240
|
+
only: {
|
|
241
|
+
handler: vi.fn().mockImplementation((ctx) => {
|
|
242
|
+
ctx.exit();
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
afterClose: {
|
|
248
|
+
on: { eventName: 'close', phase: 'afterAllFlows' },
|
|
249
|
+
steps: {
|
|
250
|
+
only: {
|
|
251
|
+
handler: vi.fn(),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
|
|
256
|
+
|
|
257
|
+
const model = createModelWithFlows('m-close-meta', flows);
|
|
258
|
+
|
|
259
|
+
const result = await engine.executor.dispatchEvent(model, 'close', {}, { sequential: true });
|
|
260
|
+
|
|
261
|
+
expect(Array.isArray(result)).toBe(true);
|
|
262
|
+
expect((result as any).__abortedByExitAll).toBe(true);
|
|
263
|
+
expect(flows.afterClose.steps.only.handler).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
169
266
|
it('dispatchEvent sequential respects sort order and stops on errors', async () => {
|
|
170
267
|
const calls: string[] = [];
|
|
171
268
|
const mkFlow = (key: string, sort: number, opts?: { throw?: boolean }) => ({
|
|
@@ -222,6 +319,32 @@ describe('FlowExecutor', () => {
|
|
|
222
319
|
expect(handler).toHaveBeenCalledTimes(2); // 每个 flow 各 1 次,共 2 次
|
|
223
320
|
});
|
|
224
321
|
|
|
322
|
+
it("dispatchEvent('beforeRender') keeps aborted flag on end event when cache hits", async () => {
|
|
323
|
+
const handler = vi.fn().mockImplementation((ctx) => {
|
|
324
|
+
ctx.exitAll();
|
|
325
|
+
});
|
|
326
|
+
const flows = {
|
|
327
|
+
abortFlow: { steps: { s: { handler } } },
|
|
328
|
+
} satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
|
|
329
|
+
const model = createModelWithFlows('m-br-cache-aborted', flows);
|
|
330
|
+
|
|
331
|
+
const endEvents: any[] = [];
|
|
332
|
+
const onEnd = (payload: any) => {
|
|
333
|
+
endEvents.push(payload);
|
|
334
|
+
};
|
|
335
|
+
engine.emitter.on('model:event:beforeRender:end', onEnd);
|
|
336
|
+
|
|
337
|
+
await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
|
|
338
|
+
await engine.executor.dispatchEvent(model, 'beforeRender', undefined, { sequential: true, useCache: true });
|
|
339
|
+
|
|
340
|
+
engine.emitter.off('model:event:beforeRender:end', onEnd);
|
|
341
|
+
|
|
342
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
343
|
+
expect(endEvents).toHaveLength(2);
|
|
344
|
+
expect(endEvents[0]?.aborted).toBe(true);
|
|
345
|
+
expect(endEvents[1]?.aborted).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
225
348
|
it('dispatchEvent supports sequential execution order and exitAll break', async () => {
|
|
226
349
|
const calls: string[] = [];
|
|
227
350
|
const mkFlow = (key: string, sort: number, opts?: { exitAll?: boolean }) => ({
|