Merge branch 'next' into develop

This commit is contained in:
nocobase[bot]
2026-02-13 06:00:05 +00:00
3 changed files with 209 additions and 0 deletions
@@ -421,6 +421,9 @@ export class FlowExecutor {
model.uid,
async (m) => {
const res = await this.runFlow(m, it.flow.key, inputArgs, runId, eventName);
if (res instanceof FlowExitAllException) {
throw res;
}
results.push(res);
},
{ when: whenKey as any },
@@ -442,6 +445,10 @@ export class FlowExecutor {
}
results.push(result);
} catch (error) {
if (error instanceof FlowExitAllException) {
logger.debug(`[FlowEngine.dispatchEvent] ${error.message}`);
break; // 终止后续
}
logger.error(
{ err: error },
`BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}' (sequential):`,
@@ -39,6 +39,45 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
expect(calls).toEqual(['dynamic', 'static-a']);
});
test('default (phase undefined): ctx.exitAll() stops static flows (beforeAllFlows regression)', async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
engine.registerModels({ M });
const calls: string[] = [];
M.registerFlow({
key: 'S',
on: { eventName: 'go' },
steps: {
a: { handler: async () => void calls.push('static-a') } as any,
},
});
M.registerFlow({
key: 'T',
on: { eventName: 'go' },
steps: {
t: { handler: async () => void calls.push('static-t') } as any,
},
});
const model = engine.createModel({ use: 'M' });
model.registerFlow('D', {
on: { eventName: 'go' },
steps: {
d: {
handler: async (ctx: any) => {
calls.push('dynamic');
ctx.exitAll();
},
} as any,
},
});
await model.dispatchEvent('go');
expect(calls).toEqual(['dynamic']);
});
test("phase='afterAllFlows': instance flow runs after static flows", async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
@@ -178,6 +217,165 @@ describe('dispatchEvent dynamic event flow phase (scheduleModelOperation integra
expect(calls).toEqual(['static-a', 'dynamic', 'static-b']);
});
test("phase='beforeFlow': ctx.exitAll() stops anchor flow and subsequent flows", async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
engine.registerModels({ M });
const calls: string[] = [];
M.registerFlow({
key: 'S',
on: { eventName: 'go' },
steps: {
a: { handler: async () => void calls.push('static-a') } as any,
},
});
M.registerFlow({
key: 'T',
on: { eventName: 'go' },
steps: {
t: { handler: async () => void calls.push('static-t') } as any,
},
});
const model = engine.createModel({ use: 'M' });
model.registerFlow('D', {
on: { eventName: 'go', phase: 'beforeFlow', flowKey: 'S' },
steps: {
d: {
handler: async (ctx: any) => {
calls.push('dynamic');
ctx.exitAll();
},
} as any,
},
});
await model.dispatchEvent('go');
expect(calls).toEqual(['dynamic']);
});
test("phase='beforeStep': ctx.exitAll() stops anchor step and subsequent flows", async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
engine.registerModels({ M });
const calls: string[] = [];
M.registerFlow({
key: 'S',
on: { eventName: 'go' },
steps: {
a: { handler: async () => void calls.push('static-a') } as any,
b: { handler: async () => void calls.push('static-b') } as any,
},
});
M.registerFlow({
key: 'T',
on: { eventName: 'go' },
steps: {
t: { handler: async () => void calls.push('static-t') } as any,
},
});
const model = engine.createModel({ use: 'M' });
model.registerFlow('D', {
on: { eventName: 'go', phase: 'beforeStep', flowKey: 'S', stepKey: 'a' },
steps: {
d: {
handler: async (ctx: any) => {
calls.push('dynamic');
ctx.exitAll();
},
} as any,
},
});
await model.dispatchEvent('go');
expect(calls).toEqual(['dynamic']);
});
test("phase='afterStep': ctx.exitAll() stops subsequent steps and subsequent flows", async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
engine.registerModels({ M });
const calls: string[] = [];
M.registerFlow({
key: 'S',
on: { eventName: 'go' },
steps: {
a: { handler: async () => void calls.push('static-a') } as any,
b: { handler: async () => void calls.push('static-b') } as any,
},
});
M.registerFlow({
key: 'T',
on: { eventName: 'go' },
steps: {
t: { handler: async () => void calls.push('static-t') } as any,
},
});
const model = engine.createModel({ use: 'M' });
model.registerFlow('D', {
on: { eventName: 'go', phase: 'afterStep', flowKey: 'S', stepKey: 'a' },
steps: {
d: {
handler: async (ctx: any) => {
calls.push('dynamic');
ctx.exitAll();
},
} as any,
},
});
await model.dispatchEvent('go');
expect(calls).toEqual(['static-a', 'dynamic']);
});
test("phase='afterFlow': ctx.exitAll() stops subsequent flows", async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
engine.registerModels({ M });
const calls: string[] = [];
M.registerFlow({
key: 'S',
on: { eventName: 'go' },
steps: {
a: { handler: async () => void calls.push('static-a') } as any,
b: { handler: async () => void calls.push('static-b') } as any,
},
});
M.registerFlow({
key: 'T',
on: { eventName: 'go' },
steps: {
t: { handler: async () => void calls.push('static-t') } as any,
},
});
const model = engine.createModel({ use: 'M' });
model.registerFlow('D', {
on: { eventName: 'go', phase: 'afterFlow', flowKey: 'S' },
steps: {
d: {
handler: async (ctx: any) => {
calls.push('dynamic');
ctx.exitAll();
},
} as any,
},
});
await model.dispatchEvent('go');
expect(calls).toEqual(['static-a', 'static-b', 'dynamic']);
});
test("phase='beforeFlow' missing flow: falls back to afterAllFlows", async () => {
const engine = new FlowEngine();
class M extends FlowModel {}
@@ -10,6 +10,7 @@
import { uid as genUid } from 'uid/secure';
import type { FlowEngine } from '../flowEngine';
import type { FlowModel } from '../models/flowModel';
import { FlowExitAllException } from '../utils/exceptions';
type LifecycleType =
| 'created'
@@ -273,6 +274,9 @@ export class ModelOperationScheduler {
if (!model) return;
await Promise.resolve(item.fn(model));
} catch (err) {
if (err instanceof FlowExitAllException) {
throw err;
}
this.engine.logger?.error?.(
{ err, id, fromUid: item.fromUid, toUid: item.toUid, when: item.options.when },
'ModelOperationScheduler: operation execution failed',