UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

906 lines (814 loc) 31.8 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { observable } from '@formily/reactive'; import _ from 'lodash'; import pino from 'pino'; import { EngineActionRegistry } from './action-registry/EngineActionRegistry'; import { EngineEventRegistry } from './event-registry/EngineEventRegistry'; import { FlowExecutor } from './executor/FlowExecutor'; import { FlowContext, FlowEngineContext, FlowRuntimeContext } from './flowContext'; import { FlowSettings } from './flowSettings'; import { ErrorFlowModel, FlowModel } from './models'; import { ReactView } from './ReactView'; import { APIResource, FlowResource, MultiRecordResource, SingleRecordResource, SQLResource } from './resources'; import type { ActionDefinition, ApplyFlowCacheEntry, CreateModelOptions, EventDefinition, FlowModelOptions, IFlowModelRepository, ModelConstructor, PersistOptions, ResourceType, } from './types'; import { isInheritedFrom } from './utils'; /** * FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more. * It provides capabilities for registering, creating, finding, persisting, replacing, and moving models. * Supports flow definitions, action definitions, model class inheritance and filtering. * Integrates FlowEngineContext, FlowSettings, and ReactView for context, configuration, and view rendering. * * Main features: * - Register, get, and find model classes and model instances * - Register and get action definitions * - Persist and query models via the model repository * - Register flow definitions * - Create, find, replace, move, and destroy model instances * - Support for model class inheritance and filtering * - Internationalization support * - Integration with React view rendering * * @example * const engine = new FlowEngine(); * engine.registerModels({ MyModel }); * engine.setModelRepository(new MyRepository()); * const model = engine.createModel({ use: 'MyModel', uid: 'xxx' }); */ export class FlowEngine { /** * Global action registry */ private _actionRegistry = new EngineActionRegistry(); /** * Global event registry */ private _eventRegistry = new EngineEventRegistry(); /** * Registered model classes. * Key is the model class name, value is the model class constructor. * @private */ private _modelClasses: Map<string, ModelConstructor> = observable.shallow(new Map()); /** * Created model instances. * Key is the model instance UID, value is the model instance object. * @private */ private _modelInstances: Map<string, any> = new Map(); /** * The current model repository instance, implements IFlowModelRepository. * Used for model persistence and queries. * @private */ private _modelRepository: IFlowModelRepository | null = null; /** * Flow application cache. * Key is the cache key, value is ApplyFlowCacheEntry. * @private */ private _applyFlowCache = new Map<string, ApplyFlowCacheEntry>(); /** * Model saving state tracking. * Key is the model UID, value is the save promise. * @private */ private _savingModels = new Map<string, Promise<any>>(); /** * Flow engine context object. * @private */ private _flowContext: FlowEngineContext; /** * 视图作用域引擎的栈式链表指针。 * - previousEngine:打开当前视图的上一个引擎 * - nextEngine:在当前之上的下一个引擎 */ private _previousEngine?: FlowEngine; private _nextEngine?: FlowEngine; private _resources = new Map<string, typeof FlowResource>(); logger: pino.Logger; /** * Flow settings, including components and form scopes. * @public */ public flowSettings: FlowSettings; /** * Experimental API: Integrates React view rendering capability into FlowEngine. * This property may change or be removed in the future. Use with caution. * @experimental * @public */ public reactView: ReactView; /** * Flow executor that runs flows and auto-flows. */ public executor: FlowExecutor; /** * Constructor. Initializes React view, registers default model and form scopes. */ constructor() { this.reactView = new ReactView(this); this.flowSettings = new FlowSettings(this); this.flowSettings.registerScopes({ t: this.translate.bind(this) }); this.registerModels({ FlowModel }); // 会造成循环依赖问题,移除掉 this.registerResources({ FlowResource, SQLResource, APIResource, SingleRecordResource, MultiRecordResource, }); this.logger = pino({ level: 'trace', browser: { write: { fatal: (o) => console.trace(o), error: (o) => console.error(o), warn: (o) => console.warn(o), info: (o) => console.info(o), debug: (o) => console.debug(o), trace: (o) => console.trace(o), }, }, }); this.executor = new FlowExecutor(this); } /** 上一个引擎(根引擎为 undefined) */ get previousEngine(): FlowEngine | undefined { return this._previousEngine; } /** 下一个引擎(若存在) */ get nextEngine(): FlowEngine | undefined { return this._nextEngine; } /** * 将当前引擎链接到 prev 之后(用于视图打开时形成栈关系)。 */ public linkAfter(engine: FlowEngine): void { // 找到栈底 let prev: FlowEngine = engine; while (prev._nextEngine) prev = prev._nextEngine; this._previousEngine = prev; if (prev) { prev._nextEngine = this; } } /** * 将当前引擎从栈中移除并修复相邻指针(用于视图关闭时)。 */ public unlinkFromStack(): void { const prev = this._previousEngine; const next = this._nextEngine; if (prev) { prev._nextEngine = undefined; } } // (已移除)getModelGlobal/forEachModelGlobal/getAllModelsGlobal:不再维护冗余全局遍历 API /** * Get the flow engine context object. * @returns {FlowEngineContext} Flow engine context */ get context() { if (!this._flowContext) { this._flowContext = new FlowEngineContext(this); } return this._flowContext; } get dataSourceManager() { return this.context.dataSourceManager; } /** * Get the flow application cache. * @returns {Map<string, ApplyFlowCacheEntry>} Flow application cache map * @internal */ get applyFlowCache() { return this._applyFlowCache; } /** * Set the model repository for persisting and querying model instances. * If a model repository was already set, it will be overwritten and a warning will be printed. * @param {IFlowModelRepository} modelRepository The model repository instance implementing IFlowModelRepository. * @example * flowEngine.setModelRepository(new MyFlowModelRepository()); */ setModelRepository(modelRepository: IFlowModelRepository) { if (this._modelRepository) { console.warn('FlowEngine: Model repository is already set and will be overwritten.'); } this._modelRepository = modelRepository; } get modelRepository(): IFlowModelRepository | null { return this._modelRepository; } /** * Internationalization translation method, calls the context's t method. * @param {string} keyOrTemplate Translation key or template string * @param {any} [options] Optional parameters * @returns {string} Translated string */ translate(keyOrTemplate: string, options?: any): string { return this.context.t(keyOrTemplate, options); } /** * Register multiple actions. * @param {Record<string, ActionDefinition>} actions Action definition object collection */ registerActions(actions: Record<string, ActionDefinition>): void { this._actionRegistry.registerActions(actions); } /** * Get a registered action definition. * @template TModel Specific FlowModel subclass type * @param {string} name Action name * @returns {ActionDefinition<TModel> | undefined} Action definition, or undefined if not found */ public getAction<TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowRuntimeContext<TModel>>( name: string, ): ActionDefinition<TModel, TCtx> | undefined { return this._actionRegistry.getAction<TModel, TCtx>(name); } /** * Get all registered global actions. * Returns a new Map to avoid external mutation of internal state. */ public getActions<TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowRuntimeContext<TModel>>(): Map< string, ActionDefinition<TModel, TCtx> > { return this._actionRegistry.getActions<TModel, TCtx>(); } /** * Register multiple events. */ registerEvents(events: Record<string, EventDefinition>): void { this._eventRegistry.registerEvents(events); } /** * Get a registered event definition. */ public getEvent<TModel extends FlowModel = FlowModel>(name: string): EventDefinition<TModel> | undefined { return this._eventRegistry.getEvent<TModel>(name); } /** * Get all registered global events. */ public getEvents<TModel extends FlowModel = FlowModel>(): Map<string, EventDefinition<TModel>> { return this._eventRegistry.getEvents<TModel>(); } /** * Register a single model class. * @param {string} name Model class name * @param {ModelConstructor} modelClass Model class constructor * @private */ #registerModel(name: string, modelClass: ModelConstructor): void { if (this._modelClasses.has(name)) { console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`); } Object.defineProperty(modelClass, 'name', { value: name }); this._modelClasses.set(name, modelClass); } /** * Register multiple model classes. * @param {Record<string, ModelConstructor>} models Model class map, key is model name, value is model constructor * @returns {void} * @example * flowEngine.registerModels({ UserModel, OrderModel }); */ public registerModels(models: Record<string, ModelConstructor | typeof FlowModel<any>>) { for (const [name, modelClass] of Object.entries(models)) { this.#registerModel(name, modelClass); } } registerResources(resources: Record<string, any>) { for (const [name, resourceClass] of Object.entries(resources)) { this._resources.set(name, resourceClass); } } createResource<T = FlowResource>(resourceType: ResourceType<T>, options?: { context?: FlowContext }): T { if (typeof resourceType === 'string') { const ResourceClass = this._resources.get(resourceType); if (!ResourceClass) { throw new Error(`Resource class '${resourceType}' not found. Please register it first.`); } return new ResourceClass((options?.context as any) || this.context) as T; } const R = resourceType; return new R(options?.context || this.context) as T; } /** * Get all registered model classes. * @returns {Map<string, ModelConstructor>} Model class map */ getModelClasses() { return this._modelClasses; } /** * Get a registered model class (constructor). * @param {string} name Model class name * @returns {ModelConstructor | undefined} Model constructor, or undefined if not found */ public getModelClass(name: string): ModelConstructor | undefined { return this._modelClasses.get(name); } /** * Find a registered model class by predicate. * @param predicate Callback function, arguments are (name, ModelClass), returns true if matched * @returns {[string, ModelConstructor] | undefined} Matched model class and name */ public findModelClass( predicate: (name: string, ModelClass: ModelConstructor) => boolean, ): [string, ModelConstructor] | undefined { for (const [name, ModelClass] of this._modelClasses) { if (predicate(name, ModelClass)) { return [name, ModelClass]; } } return undefined; } /** * Filter model classes by base class (supports multi-level inheritance), with optional custom filter. * @param {string | ModelConstructor} baseClass Base class name or constructor * @param {(ModelClass: ModelConstructor, className: string) => boolean} [filter] Optional filter function * @returns {Map<string, ModelConstructor>} Model classes inherited from base class and passed the filter */ public getSubclassesOf( baseClass: string | ModelConstructor, filter?: (ModelClass: ModelConstructor, className: string) => boolean, ): Map<string, ModelConstructor> { const parentModelClass = typeof baseClass === 'string' ? this.getModelClass(baseClass) : baseClass; const result = new Map<string, ModelConstructor>(); if (!parentModelClass) return result; for (const [className, ModelClass] of this._modelClasses) { if (isInheritedFrom(ModelClass, parentModelClass)) { if (!filter || filter(ModelClass, className)) { result.set(className, ModelClass); } } } return result; } /** * Create and register a model instance. * If an instance with the same UID exists, returns the existing instance. * @template T FlowModel subclass type, defaults to FlowModel. * @param {CreateModelOptions} options Model creation options * @returns {T} Created model instance */ public createModel<T extends FlowModel = FlowModel>( options: CreateModelOptions, extra?: { delegateToParent?: boolean; delegate?: FlowContext }, ): T { const { parentId, uid, use: modelClassName, subModels } = options; const ModelClass = typeof modelClassName === 'string' ? this.getModelClass(modelClassName) : modelClassName; if (uid && this._modelInstances.has(uid)) { return this._modelInstances.get(uid) as T; } let modelInstance: T | ErrorFlowModel; if (!ModelClass) { modelInstance = new ErrorFlowModel({ ...options, flowEngine: this } as any); modelInstance.setErrorMessage(`Model class '${modelClassName}' not found. Please register it first.`); } else { modelInstance = new (ModelClass as ModelConstructor<T>)({ ...options, flowEngine: this } as any); } if (extra?.delegate) { modelInstance.context.addDelegate(extra.delegate); } if (parentId && this._modelInstances.has(parentId)) { modelInstance.setParent(this._modelInstances.get(parentId)); if (extra?.delegateToParent === false) { modelInstance.removeParentDelegate(); } } this._modelInstances.set(modelInstance.uid, modelInstance); modelInstance.onInit(options); // 在模型实例化阶段应用 flow 级 defaultParams(仅填充缺失的 stepParams,不覆盖) // 不阻塞创建流程:允许 defaultParams 为异步函数 this._applyFlowDefinitionDefaultParams(modelInstance as FlowModel).catch((err) => { console.warn('FlowEngine: apply flow defaultParams failed:', err); }); modelInstance._createSubModels(options.subModels); return modelInstance as T; } /** * 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。 * 仅对尚未存在的步骤参数进行填充,不覆盖已有值。 */ async _applyFlowDefinitionDefaultParams(model: FlowModel) { try { const flows = model.getFlows(); if (!flows || flows.size === 0) return; const ctx = model.context; for (const [flowKey, flowDef] of flows.entries()) { const dp = flowDef.defaultParams; if (!dp) continue; let resolved: any; try { resolved = typeof dp === 'function' ? await dp(ctx) : dp; } catch (e) { console.warn(`FlowEngine: resolve defaultParams of flow '${flowKey}' failed:`, e); continue; } if (!resolved || typeof resolved !== 'object') continue; // 仅支持:返回当前 flow 的步骤对象 { [stepKey]: params } const stepsMap = (flowDef as any).getSteps?.() as Map<string, any> | undefined; const stepKeys = stepsMap ? Array.from(stepsMap.keys()) : Object.keys(flowDef.steps || {}); const entries = Object.entries(resolved).filter(([k]) => stepKeys.includes(k)); if (entries.length === 0) continue; for (const [stepKey, params] of entries) { const exists = model.getStepParams(flowKey, stepKey as string); if (exists === undefined && params && typeof params === 'object') { model.setStepParams(flowKey, stepKey as string, params as any); } } } } catch (error) { console.warn('FlowEngine: apply flow defaultParams error:', error); } } /** * Get a model instance by UID. * @template T FlowModel subclass type, defaults to FlowModel. * @param {string} uid Model instance UID * @returns {T | undefined} Model instance, or undefined if not found */ public getModel<T extends FlowModel = FlowModel>(uid: string, global?: boolean): T | undefined { // 默认仅在当前引擎查找;只有当 global === true 时才跨视图栈查找 if (!global) { return this._modelInstances.get(uid) as T | undefined; } // 跨视图栈查找:按视图栈从栈顶到根逐个查找 // 1) 找到栈顶引擎 let top: FlowEngine = this; while (top.nextEngine) top = top.nextEngine; // 2) 从栈顶向下查找,命中即返回 let eng: FlowEngine | undefined = top; while (eng) { const found = eng._modelInstances.get(uid) as T | undefined; if (found) return found; eng = eng.previousEngine; } return undefined; } /** * Iterate all registered model instances. * @template T FlowModel subclass type, defaults to FlowModel. * @param {(model: T) => void} callback Callback function */ forEachModel<T extends FlowModel = FlowModel>(callback: (model: T) => void): void { this._modelInstances.forEach(callback); } /** * Remove a local model instance. * @param {string} uid UID of the model instance to destroy * @returns {boolean} Returns true if successfully destroyed, false otherwise (e.g. instance does not exist) */ public removeModel(uid: string): boolean { if (!this._modelInstances.has(uid)) { console.warn(`FlowEngine: Model with UID '${uid}' does not exist.`); return false; } const modelInstance = this._modelInstances.get(uid) as FlowModel; modelInstance.clearForks(); // 从父模型中移除当前模型的引用 if (modelInstance.parent?.subModels) { for (const subKey in modelInstance.parent.subModels) { const subModelValue = modelInstance.parent.subModels[subKey]; if (Array.isArray(subModelValue)) { const index = subModelValue.findIndex((subModel) => subModel == modelInstance); if (index !== -1) { subModelValue.splice(index, 1); modelInstance.parent.emitter.emit('onSubModelRemoved', modelInstance); break; } } else if (subModelValue && subModelValue === modelInstance) { delete modelInstance.parent.subModels[subKey]; modelInstance.parent.emitter.emit('onSubModelRemoved', modelInstance); break; } } } this._modelInstances.delete(uid); return true; } /** * Check if the model repository is set. * @returns {boolean} Returns true if set, false otherwise. * @private */ private ensureModelRepository(): boolean { if (!this._modelRepository) { // 不抛错,直接返回 false return false; } return true; } /** * Load a model instance (prefers local, falls back to repository). * @template T FlowModel subclass type, defaults to FlowModel. * @param {any} options Load options * @returns {Promise<T | null>} Loaded model instance or null */ async loadModel<T extends FlowModel = FlowModel>(options): Promise<T | null> { if (!this.ensureModelRepository()) return; const model = this.findModelByParentId(options.parentId, options.subKey); if (model) { return model as T; } const data = await this._modelRepository.findOne(options); return data?.uid ? this.createModel<T>(data as any) : null; } /** * Find a sub-model by parent model ID and subKey. * @template T FlowModel subclass type, defaults to FlowModel. * @param {string} parentId Parent model UID * @param {string} subKey Sub-model key * @returns {T | null} Found sub-model or null */ findModelByParentId<T extends FlowModel = FlowModel>(parentId: string, subKey: string): T | null { if (parentId && this._modelInstances.has(parentId)) { const parentModel = this._modelInstances.get(parentId) as FlowModel; if (parentModel && parentModel.subModels[subKey]) { const subModels = parentModel.subModels[subKey]; if (Array.isArray(subModels)) { return subModels[0] as T; // 返回第一个子模型 } else { return subModels as T; } } } } /** * Load or create a model instance. * @template T FlowModel subclass type, defaults to FlowModel. * @param {any} options Load or create options * @returns {Promise<T | null>} Model instance or null */ async loadOrCreateModel<T extends FlowModel = FlowModel>( options, extra?: { delegateToParent?: boolean; delegate?: FlowContext; }, ): Promise<T | null> { if (!this.ensureModelRepository()) return; const { uid, parentId, subKey } = options; if (uid && this._modelInstances.has(uid)) { return this._modelInstances.get(uid) as T; } const m = this.findModelByParentId<T>(parentId, subKey); if (m) { return m; } const data = await this._modelRepository.findOne(options); let model: T | null = null; if (data?.uid) { model = this.createModel<T>(data as any, extra); } else { model = this.createModel<T>(options, extra); await model.save(); } if (model.parent) { const subModel = model.parent.findSubModel(model.subKey, (m) => { return m.uid === model.uid; }); if (subModel) { return model; } if (model.subType === 'array') { model.parent.addSubModel(model.subKey, model); } else { model.parent.setSubModel(model.subKey, model); } } return model; } /** * Persist and save a model instance. * Prevents concurrent saves of the same model by tracking save operations. * If a model is already being saved, subsequent calls will wait for the existing save to complete. * * @template T FlowModel subclass type, defaults to FlowModel. * @param {T} model Model instance to save * @param {object} [options] Save options * @param {boolean} [options.onlyStepParams] Whether to save only step parameters * @returns {Promise<any>} Repository save result */ async saveModel<T extends FlowModel = FlowModel>(model: T, options?: { onlyStepParams?: boolean }): Promise<any> { if (!this.ensureModelRepository()) return; const modelUid = model.uid; // 如果这个 model 正在保存中,返回现有的保存 Promise if (this._savingModels.has(modelUid)) { this.logger.debug(`Model ${modelUid} is already being saved, waiting for existing save operation`); return await this._savingModels.get(modelUid); } // 创建保存 Promise 并添加到追踪 Map 中 const savePromise = this._performModelSave(model, options); this._savingModels.set(modelUid, savePromise); try { const result = await savePromise; return result; } finally { // 无论成功还是失败,都要清除保存状态 this._savingModels.delete(modelUid); } } /** * Perform the actual model save operation. * @template T FlowModel subclass type, defaults to FlowModel. * @param {T} model Model instance to save * @param {object} [options] Save options * @returns {Promise<any>} Repository save result * @private */ async _performModelSave<T extends FlowModel = FlowModel>( model: T, options?: { onlyStepParams?: boolean }, ): Promise<any> { this.logger.debug(`Starting save operation for model ${model.uid}`); try { const result = await this._modelRepository.save(model, options); this.logger.debug(`Successfully saved model ${model.uid}`); return result; } catch (error) { this.logger.error(`Failed to save model ${model.uid}:`, error); throw error; } } /** * Destroy a model instance (persistently delete and remove local instance). * @param {string} uid UID of the model to destroy * @returns {Promise<boolean>} Whether destroyed successfully */ async destroyModel(uid: string) { if (this.ensureModelRepository()) { await this._modelRepository.destroy(uid); } return this.removeModel(uid); } /** * Replace a model instance with a new instance of a class. * @template T New model type * @param {string} uid UID of the model to replace * @param {Partial<FlowModelOptions> | ((oldModel: FlowModel) => Partial<FlowModelOptions>)} [optionsOrFn] * Options for creating the new model, supports two forms: * 1. Pass options directly * 2. Pass a function that receives current options and returns new options * @returns {Promise<T | null>} Newly created model instance */ async replaceModel<T extends FlowModel = FlowModel>( uid: string, optionsOrFn?: Partial<FlowModelOptions> | ((currentOptions: FlowModelOptions) => Partial<FlowModelOptions>), ): Promise<T | null> { const oldModel = this.getModel(uid); if (!oldModel) { console.warn(`FlowEngine: Cannot replace model. Model with UID '${uid}' not found.`); return null; } // 1. 保存当前模型的关键信息 const currentParent = oldModel.parent; const currentSubKey = oldModel.subKey; const currentSubType = oldModel.subType; const currentOptions = oldModel.serialize(); // 2. 确定新的选项 let userOptions: Partial<FlowModelOptions>; if (typeof optionsOrFn === 'function') { // 函数模式:传入当前options,获取新的options userOptions = optionsOrFn(oldModel as any); } else { // 对象模式:直接使用提供的options替换 userOptions = optionsOrFn || {}; } // 3. 合并用户选项和关键属性 const newOptions = { ..._.omit(currentOptions, ['subModels']), ...userOptions, } as CreateModelOptions; // 暂停父模型的事件触发, // TODO: find a better way to do this if (currentParent) { currentParent.emitter.setPaused(true); } // 4. 销毁当前模型(这会处理所有清理工作:持久化删除、内存清理、父模型引用等) await oldModel.destroy(); // 5. 使用createModel创建新的模型实例 const newModel = this.createModel<T>(newOptions); // 6. 如果有父模型,将新模型添加到父模型的subModels中 if (currentParent && currentSubKey) { if (currentSubType === 'array') { // 对于数组类型,使用addSubModel方法 currentParent.addSubModel(currentSubKey, newModel); } else { // 对于对象类型,使用setSubModel方法 currentParent.setSubModel(currentSubKey, newModel); } } // 7. 触发事件以通知其他部分模型已替换 if (currentParent) { currentParent.emitter.setPaused(false); currentParent.parent.invalidateAutoFlowCache(true); currentParent.parent?.rerender(); currentParent.emitter.emit('onSubModelReplaced', { oldModel, newModel }); } await newModel.save(); return newModel; } /** * Move a model instance within its parent model. * @param {any} sourceId Source model UID * @param {any} targetId Target model UID * @returns {Promise<void>} No return value */ async moveModel(sourceId: any, targetId: any, options?: PersistOptions): Promise<void> { const sourceModel = this.getModel(sourceId); const targetModel = this.getModel(targetId); if (!sourceModel || !targetModel) { console.warn(`FlowEngine: Cannot move model. Source or target model not found.`); return; } const move = (sourceModel: FlowModel, targetModel: FlowModel) => { if (!sourceModel.parent || !targetModel.parent || sourceModel.parent !== targetModel.parent) { console.error('FlowModel.moveTo: Both models must have the same parent to perform move operation.'); return false; } const subModels = sourceModel.parent.subModels[sourceModel.subKey]; if (!subModels || !Array.isArray(subModels)) { console.error('FlowModel.moveTo: Parent subModels must be an array to perform move operation.'); return false; } const findIndex = (model: FlowModel) => subModels.findIndex((item) => item.uid === model.uid); const currentIndex = findIndex(sourceModel); const targetIndex = findIndex(targetModel); if (currentIndex === -1 || targetIndex === -1) { console.error('FlowModel.moveTo: Current or target model not found in parent subModels.'); return false; } if (currentIndex === targetIndex) { console.warn('FlowModel.moveTo: Current model is already at the target position. No action taken.'); return false; } // 使用splice直接移动数组元素(O(n)比排序O(n log n)更快) const [movedModel] = subModels.splice(currentIndex, 1); subModels.splice(targetIndex, 0, movedModel); // 重新分配连续的sortIndex subModels.forEach((model, index) => { model.sortIndex = index; }); return true; }; move(sourceModel, targetModel); if (options?.persist !== false && this.ensureModelRepository()) { const position = sourceModel.sortIndex - targetModel.sortIndex > 0 ? 'after' : 'before'; await this._modelRepository.move(sourceId, targetId, position); } // 触发事件以通知其他部分模型已移动 sourceModel.parent.emitter.emit('onSubModelMoved', { source: sourceModel, target: targetModel }); } /** * Filter model classes by parent class (supports multi-level inheritance). * @param {string | ModelConstructor} parentClass Parent class name or constructor * @returns {Map<string, ModelConstructor>} Model classes inherited from the specified parent class */ public filterModelClassByParent(parentClass: string | ModelConstructor) { const parentModelClass = typeof parentClass === 'string' ? this.getModelClass(parentClass) : parentClass; if (!parentModelClass) { return new Map(); } const modelClasses = new Map<string, ModelConstructor>(); for (const [className, ModelClass] of this._modelClasses) { if (isInheritedFrom(ModelClass, parentModelClass)) { modelClasses.set(className, ModelClass); } } return modelClasses; } /** * Generate a unique key for the flow application cache. * @param {string} prefix Prefix * @param {string} flowKey Flow key * @param {string} modelUid Model UID * @returns {string} Unique cache key * @internal */ static generateApplyFlowCacheKey(prefix: string, flowKey: string, modelUid: string): string { return `${prefix}:${flowKey}:${modelUid}`; } }