This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { inject, optional } from 'inversify';
|
||||
import { Scope, ScopeChain } from '@flowgram.ai/variable-core';
|
||||
import { FlowDocument, type FlowVirtualTree } from '@flowgram.ai/document';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { VariableChainConfig } from '../variable-chain-config';
|
||||
import { FlowNodeScope, FlowNodeScopeTypeEnum, ScopeChainNode } from '../types';
|
||||
import { ScopeChainTransformService } from '../services/scope-chain-transform-service';
|
||||
import { GlobalScope } from '../scopes/global-scope';
|
||||
import { FlowNodeVariableData } from '../flow-node-variable-data';
|
||||
|
||||
/**
|
||||
* Scope chain implementation based on `FlowVirtualTree`.
|
||||
*/
|
||||
export class FixedLayoutScopeChain extends ScopeChain {
|
||||
// By adding { id: string }, custom virtual nodes can be flexibly added
|
||||
tree: FlowVirtualTree<ScopeChainNode> | undefined;
|
||||
|
||||
@inject(ScopeChainTransformService)
|
||||
protected transformService: ScopeChainTransformService;
|
||||
|
||||
constructor(
|
||||
@inject(FlowDocument)
|
||||
protected flowDocument: FlowDocument,
|
||||
@optional()
|
||||
@inject(VariableChainConfig)
|
||||
protected configs?: VariableChainConfig
|
||||
) {
|
||||
super();
|
||||
|
||||
// Bind the tree in flowDocument
|
||||
this.bindTree(flowDocument.originTree);
|
||||
|
||||
// When originTree changes, trigger changes in dependencies
|
||||
this.toDispose.push(
|
||||
// REFRACTOR: onTreeChange trigger timing needs to be refined
|
||||
flowDocument.originTree.onTreeChange(() => {
|
||||
this.refreshAllChange();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the scope chain to a `FlowVirtualTree`.
|
||||
* @param tree The `FlowVirtualTree` to bind to.
|
||||
*/
|
||||
bindTree(tree: FlowVirtualTree<ScopeChainNode>): void {
|
||||
this.tree = tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dependency scopes for a given scope.
|
||||
* @param scope The scope to get dependencies for.
|
||||
* @returns An array of dependency scopes.
|
||||
*/
|
||||
getDeps(scope: FlowNodeScope): FlowNodeScope[] {
|
||||
if (!this.tree) {
|
||||
return this.transformService.transformDeps([], { scope });
|
||||
}
|
||||
|
||||
const node = scope.meta.node;
|
||||
if (!node) {
|
||||
return this.transformService.transformDeps([], { scope });
|
||||
}
|
||||
|
||||
const deps: FlowNodeScope[] = [];
|
||||
|
||||
let curr: ScopeChainNode | undefined = node;
|
||||
|
||||
while (curr) {
|
||||
const { parent, pre } = this.tree.getInfo(curr);
|
||||
const currData = this.getVariableData(curr);
|
||||
|
||||
// Contains child nodes and is not a private scope
|
||||
|
||||
if (curr === node) {
|
||||
// public can depend on private
|
||||
if (scope.meta.type === FlowNodeScopeTypeEnum.public && currData?.private) {
|
||||
deps.unshift(currData.private);
|
||||
}
|
||||
} else if (this.hasChildren(curr) && !this.isNodeChildrenPrivate(curr)) {
|
||||
// For nodes with child elements, include the child elements in the dependency scope
|
||||
deps.unshift(
|
||||
...this.getAllSortedChildScope(curr, {
|
||||
ignoreNodeChildrenPrivate: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// The public of the node can be accessed
|
||||
if (currData && curr !== node) {
|
||||
deps.unshift(currData.public);
|
||||
}
|
||||
|
||||
// Process the previous node
|
||||
if (pre) {
|
||||
curr = pre;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process the parent node
|
||||
if (parent) {
|
||||
let currParent: ScopeChainNode | undefined = parent;
|
||||
let currParentPre: ScopeChainNode | undefined = this.tree.getPre(currParent);
|
||||
|
||||
while (currParent) {
|
||||
// Both private and public of the parent node can be accessed by child nodes
|
||||
const currParentData = this.getVariableData(currParent);
|
||||
if (currParentData) {
|
||||
deps.unshift(...currParentData.allScopes);
|
||||
}
|
||||
|
||||
// If the current parent has a pre node, stop searching upwards
|
||||
if (currParentPre) {
|
||||
break;
|
||||
}
|
||||
|
||||
currParent = this.tree.getParent(currParent);
|
||||
currParentPre = currParent ? this.tree.getPre(currParent) : undefined;
|
||||
}
|
||||
curr = currParentPre;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there is no next and no parent, end the loop directly
|
||||
curr = undefined;
|
||||
}
|
||||
|
||||
// If scope is GlobalScope, add globalScope to deps
|
||||
const globalScope = this.variableEngine.getScopeById(GlobalScope.ID);
|
||||
if (globalScope) {
|
||||
deps.unshift(globalScope);
|
||||
}
|
||||
|
||||
return this.transformService.transformDeps(deps, { scope });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the covering scopes for a given scope.
|
||||
* @param scope The scope to get covering scopes for.
|
||||
* @returns An array of covering scopes.
|
||||
*/
|
||||
getCovers(scope: FlowNodeScope): FlowNodeScope[] {
|
||||
if (!this.tree) {
|
||||
return this.transformService.transformCovers([], { scope });
|
||||
}
|
||||
|
||||
// If scope is GlobalScope, return all scopes except GlobalScope
|
||||
if (GlobalScope.is(scope)) {
|
||||
const scopes = this.variableEngine
|
||||
.getAllScopes({ sort: true })
|
||||
.filter((_scope) => !GlobalScope.is(_scope));
|
||||
|
||||
return this.transformService.transformCovers(scopes, { scope });
|
||||
}
|
||||
|
||||
const node = scope.meta.node;
|
||||
if (!node) {
|
||||
return this.transformService.transformCovers([], { scope });
|
||||
}
|
||||
|
||||
const covers: FlowNodeScope[] = [];
|
||||
|
||||
// If it is a private scope, only child nodes can access it
|
||||
if (scope.meta.type === FlowNodeScopeTypeEnum.private) {
|
||||
covers.push(
|
||||
...this.getAllSortedChildScope(node, {
|
||||
addNodePrivateScope: true,
|
||||
}).filter((_scope) => _scope !== scope)
|
||||
);
|
||||
return this.transformService.transformCovers(covers, { scope });
|
||||
}
|
||||
|
||||
let curr: ScopeChainNode | undefined = node;
|
||||
|
||||
while (curr) {
|
||||
const { next, parent } = this.tree.getInfo(curr);
|
||||
const currData = this.getVariableData(curr);
|
||||
|
||||
// For nodes with child elements, include the child elements in the covering scope
|
||||
if (curr !== node) {
|
||||
if (this.hasChildren(curr)) {
|
||||
covers.push(
|
||||
...this.getAllSortedChildScope(curr, {
|
||||
addNodePrivateScope: true,
|
||||
})
|
||||
);
|
||||
} else if (currData) {
|
||||
covers.push(...currData.allScopes);
|
||||
}
|
||||
}
|
||||
|
||||
// Process the next node
|
||||
if (next) {
|
||||
curr = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
let currParent: ScopeChainNode | undefined = parent;
|
||||
let currParentNext: ScopeChainNode | undefined = this.tree.getNext(currParent);
|
||||
|
||||
while (currParent) {
|
||||
// Private scopes cannot be accessed by subsequent nodes
|
||||
if (this.isNodeChildrenPrivate(currParent)) {
|
||||
return this.transformService.transformCovers(covers, { scope });
|
||||
}
|
||||
|
||||
// If the current parent has a next node, stop searching upwards
|
||||
if (currParentNext) {
|
||||
break;
|
||||
}
|
||||
|
||||
currParent = this.tree.getParent(currParent);
|
||||
currParentNext = currParent ? this.tree.getNext(currParent) : undefined;
|
||||
}
|
||||
if (!currParentNext && currParent) {
|
||||
break;
|
||||
}
|
||||
|
||||
curr = currParentNext;
|
||||
continue;
|
||||
}
|
||||
|
||||
// next 和 parent 都没有,直接结束循环
|
||||
curr = undefined;
|
||||
}
|
||||
|
||||
return this.transformService.transformCovers(covers, { scope });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts all scopes in the scope chain.
|
||||
* @returns A sorted array of all scopes.
|
||||
*/
|
||||
sortAll(): Scope[] {
|
||||
const startNode = this.flowDocument.getAllNodes().find((_node) => _node.isStart);
|
||||
if (!startNode) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const startVariableData = startNode.getData(FlowNodeVariableData);
|
||||
const startPublicScope = startVariableData.public;
|
||||
const deps = this.getDeps(startPublicScope);
|
||||
|
||||
const covers = this.getCovers(startPublicScope).filter(
|
||||
(_scope) => !deps.includes(_scope) && _scope !== startPublicScope
|
||||
);
|
||||
|
||||
return [...deps, startPublicScope, ...covers];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `FlowNodeVariableData` for a given `ScopeChainNode`.
|
||||
* @param node The `ScopeChainNode` to get data for.
|
||||
* @returns The `FlowNodeVariableData` or `undefined` if not found.
|
||||
*/
|
||||
private getVariableData(node: ScopeChainNode): FlowNodeVariableData | undefined {
|
||||
if (node.flowNodeType === 'virtualNode') {
|
||||
return;
|
||||
}
|
||||
// TODO Nodes containing $ do not register variableData
|
||||
if (node.id.startsWith('$')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (node as FlowNodeEntity).getData(FlowNodeVariableData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the children of a node are private.
|
||||
* @param node The node to check.
|
||||
* @returns `true` if the children are private, `false` otherwise.
|
||||
*/
|
||||
private isNodeChildrenPrivate(node?: ScopeChainNode): boolean {
|
||||
if (this.configs?.isNodeChildrenPrivate) {
|
||||
return node ? this.configs?.isNodeChildrenPrivate(node) : false;
|
||||
}
|
||||
|
||||
const isSystemNode = node?.id.startsWith('$');
|
||||
// Fallback: all nodes with children (node id does not start with $) are private scopes
|
||||
return !isSystemNode && this.hasChildren(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node has children.
|
||||
* @param node The node to check.
|
||||
* @returns `true` if the node has children, `false` otherwise.
|
||||
*/
|
||||
private hasChildren(node?: ScopeChainNode): boolean {
|
||||
return Boolean(this.tree && node && this.tree.getChildren(node).length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all sorted child scopes of a node.
|
||||
* @param node The node to get child scopes for.
|
||||
* @param options Options for getting child scopes.
|
||||
* @returns An array of sorted child scopes.
|
||||
*/
|
||||
private getAllSortedChildScope(
|
||||
node: ScopeChainNode,
|
||||
{
|
||||
ignoreNodeChildrenPrivate,
|
||||
addNodePrivateScope,
|
||||
}: { ignoreNodeChildrenPrivate?: boolean; addNodePrivateScope?: boolean } = {}
|
||||
): FlowNodeScope[] {
|
||||
const scopes: FlowNodeScope[] = [];
|
||||
|
||||
const variableData = this.getVariableData(node);
|
||||
|
||||
if (variableData) {
|
||||
scopes.push(variableData.public);
|
||||
}
|
||||
|
||||
// For private scopes, the variables of child nodes are not exposed externally
|
||||
// (If the parent node has public variables, they are exposed externally)
|
||||
if (ignoreNodeChildrenPrivate && this.isNodeChildrenPrivate(node)) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
if (addNodePrivateScope && variableData?.private) {
|
||||
scopes.push(variableData.private);
|
||||
}
|
||||
|
||||
const children = this.tree?.getChildren(node) || [];
|
||||
scopes.push(
|
||||
...children
|
||||
.map((child) =>
|
||||
this.getAllSortedChildScope(child, { ignoreNodeChildrenPrivate, addNodePrivateScope })
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
|
||||
return scopes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { inject, optional, postConstruct } from 'inversify';
|
||||
import { Scope, ScopeChain } from '@flowgram.ai/variable-core';
|
||||
import { WorkflowNodeLinesData, WorkflowNodeMeta } from '@flowgram.ai/free-layout-core';
|
||||
import {
|
||||
FlowNodeEntity,
|
||||
FlowDocument,
|
||||
FlowVirtualTree,
|
||||
FlowNodeBaseType,
|
||||
} from '@flowgram.ai/document';
|
||||
import { EntityManager } from '@flowgram.ai/core';
|
||||
|
||||
import { VariableChainConfig } from '../variable-chain-config';
|
||||
import { FlowNodeScope, FlowNodeScopeTypeEnum } from '../types';
|
||||
import { ScopeChainTransformService } from '../services/scope-chain-transform-service';
|
||||
import { GlobalScope } from '../scopes/global-scope';
|
||||
import { FlowNodeVariableData } from '../flow-node-variable-data';
|
||||
|
||||
/**
|
||||
* Scope chain implementation for free layout.
|
||||
*/
|
||||
export class FreeLayoutScopeChain extends ScopeChain {
|
||||
@inject(EntityManager) entityManager: EntityManager;
|
||||
|
||||
@inject(FlowDocument)
|
||||
protected flowDocument: FlowDocument;
|
||||
|
||||
@optional()
|
||||
@inject(VariableChainConfig)
|
||||
protected configs?: VariableChainConfig;
|
||||
|
||||
@inject(ScopeChainTransformService)
|
||||
protected transformService: ScopeChainTransformService;
|
||||
|
||||
/**
|
||||
* The virtual tree of the flow document.
|
||||
*/
|
||||
get tree(): FlowVirtualTree<FlowNodeEntity> {
|
||||
return this.flowDocument.originTree;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
onInit() {
|
||||
this.toDispose.pushAll([
|
||||
// When the line changes, the scope chain will be updated
|
||||
this.entityManager.onEntityDataChange(({ entityDataType }) => {
|
||||
if (entityDataType === WorkflowNodeLinesData.type) {
|
||||
this.refreshAllChange();
|
||||
}
|
||||
}),
|
||||
// Refresh the scope when the tree changes
|
||||
this.tree.onTreeChange(() => {
|
||||
this.refreshAllChange();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all input layer nodes for a given node in the same layer, sorted by distance.
|
||||
* @param node The node to get input layer nodes for.
|
||||
* @returns An array of input layer nodes.
|
||||
*/
|
||||
protected getAllInputLayerNodes(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||
const currParent = this.getNodeParent(node);
|
||||
|
||||
const result = new Set<FlowNodeEntity>();
|
||||
|
||||
// add by bfs
|
||||
const queue: FlowNodeEntity[] = [node];
|
||||
|
||||
while (queue.length) {
|
||||
const curr = queue.shift()!;
|
||||
|
||||
(curr.lines?.inputNodes || []).forEach((inputNode) => {
|
||||
if (this.getNodeParent(inputNode) === currParent) {
|
||||
if (result.has(inputNode)) {
|
||||
return;
|
||||
}
|
||||
queue.push(inputNode);
|
||||
result.add(inputNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(result).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all output layer nodes for a given node in the same layer.
|
||||
* @param curr The node to get output layer nodes for.
|
||||
* @returns An array of output layer nodes.
|
||||
*/
|
||||
protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {
|
||||
const currParent = this.getNodeParent(curr);
|
||||
|
||||
return (curr.lines?.allOutputNodes || []).filter(
|
||||
(_node) => this.getNodeParent(_node) === currParent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dependency scopes for a given scope.
|
||||
* @param scope The scope to get dependencies for.
|
||||
* @returns An array of dependency scopes.
|
||||
*/
|
||||
getDeps(scope: FlowNodeScope): FlowNodeScope[] {
|
||||
const { node } = scope.meta || {};
|
||||
if (!node) {
|
||||
return this.transformService.transformDeps([], { scope });
|
||||
}
|
||||
|
||||
const deps: FlowNodeScope[] = [];
|
||||
|
||||
// 1. find dep nodes
|
||||
let curr: FlowNodeEntity | undefined = node;
|
||||
|
||||
while (curr) {
|
||||
// 2. private scope of parent node can be access
|
||||
const currVarData: FlowNodeVariableData = curr.getData(FlowNodeVariableData);
|
||||
if (currVarData?.private && scope !== currVarData.private) {
|
||||
deps.unshift(currVarData.private);
|
||||
}
|
||||
|
||||
// 3. all public scopes of inputNodes
|
||||
const allInputNodes: FlowNodeEntity[] = this.getAllInputLayerNodes(curr);
|
||||
deps.unshift(
|
||||
...allInputNodes
|
||||
.map((_node) => [
|
||||
_node.getData(FlowNodeVariableData).public,
|
||||
// 4. all public children of inputNodes
|
||||
...this.getAllPublicChildScopes(_node),
|
||||
])
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
curr = this.getNodeParent(curr);
|
||||
}
|
||||
|
||||
// If scope is GlobalScope, add globalScope to deps
|
||||
const globalScope = this.variableEngine.getScopeById(GlobalScope.ID);
|
||||
if (globalScope) {
|
||||
deps.unshift(globalScope);
|
||||
}
|
||||
|
||||
const uniqDeps = Array.from(new Set(deps));
|
||||
return this.transformService.transformDeps(uniqDeps, { scope });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the covering scopes for a given scope.
|
||||
* @param scope The scope to get covering scopes for.
|
||||
* @returns An array of covering scopes.
|
||||
*/
|
||||
getCovers(scope: FlowNodeScope): FlowNodeScope[] {
|
||||
// If scope is GlobalScope, return all scopes except GlobalScope
|
||||
if (GlobalScope.is(scope)) {
|
||||
const scopes = this.variableEngine
|
||||
.getAllScopes({ sort: true })
|
||||
.filter((_scope) => !GlobalScope.is(_scope));
|
||||
|
||||
return this.transformService.transformCovers(scopes, { scope });
|
||||
}
|
||||
|
||||
const { node } = scope.meta || {};
|
||||
if (!node) {
|
||||
return this.transformService.transformCovers([], { scope });
|
||||
}
|
||||
|
||||
const isPrivate = scope.meta.type === FlowNodeScopeTypeEnum.private;
|
||||
|
||||
// 1. BFS to find all covered nodes
|
||||
const queue: FlowNodeEntity[] = [];
|
||||
|
||||
if (isPrivate) {
|
||||
// private can only cover its child nodes
|
||||
queue.push(...this.getNodeChildren(node));
|
||||
} else {
|
||||
// Otherwise, cover all nodes of its output lines
|
||||
queue.push(...(this.getAllOutputLayerNodes(node) || []));
|
||||
|
||||
// get all parents
|
||||
let parent = this.getNodeParent(node);
|
||||
|
||||
while (parent) {
|
||||
// if childNodes of parent is private to next nodes, break
|
||||
if (this.isNodeChildrenPrivate(parent)) {
|
||||
break;
|
||||
}
|
||||
|
||||
queue.push(...this.getAllOutputLayerNodes(parent));
|
||||
|
||||
parent = this.getNodeParent(parent);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get the public and private scopes of all covered nodes
|
||||
const scopes: FlowNodeScope[] = [];
|
||||
|
||||
while (queue.length) {
|
||||
const _node = queue.shift()!;
|
||||
const variableData: FlowNodeVariableData = _node.getData(FlowNodeVariableData);
|
||||
scopes.push(...variableData.allScopes);
|
||||
const children = _node && this.getNodeChildren(_node);
|
||||
|
||||
if (children?.length) {
|
||||
queue.push(...children);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If the current scope is private, the public scope of the current node can also be covered
|
||||
const currentVariableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);
|
||||
if (isPrivate && currentVariableData.public) {
|
||||
scopes.push(currentVariableData.public);
|
||||
}
|
||||
|
||||
const uniqScopes = Array.from(new Set(scopes));
|
||||
|
||||
return this.transformService.transformCovers(uniqScopes, { scope });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the children of a node.
|
||||
* @param node The node to get children for.
|
||||
* @returns An array of child nodes.
|
||||
*/
|
||||
getNodeChildren(node: FlowNodeEntity): FlowNodeEntity[] {
|
||||
if (this.configs?.getNodeChildren) {
|
||||
return this.configs.getNodeChildren?.(node);
|
||||
}
|
||||
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(node);
|
||||
|
||||
if (subCanvas) {
|
||||
// The sub-canvas itself does not have children
|
||||
if (subCanvas.isCanvas) {
|
||||
return [];
|
||||
} else {
|
||||
return subCanvas.canvasNode.collapsedChildren;
|
||||
}
|
||||
}
|
||||
|
||||
// In some scenarios, the parent-child relationship is expressed through connections, so it needs to be configured at the upper level
|
||||
return this.tree.getChildren(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get All children of nodes
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
getAllPublicChildScopes(node: FlowNodeEntity): Scope[] {
|
||||
if (this.isNodeChildrenPrivate(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.getNodeChildren(node)
|
||||
.map((_node) => [
|
||||
_node.getData(FlowNodeVariableData).public,
|
||||
...this.getAllPublicChildScopes(_node),
|
||||
])
|
||||
.flat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent of a node.
|
||||
* @param node The node to get the parent for.
|
||||
* @returns The parent node or `undefined` if not found.
|
||||
*/
|
||||
getNodeParent(node: FlowNodeEntity): FlowNodeEntity | undefined {
|
||||
// In some scenarios, the parent-child relationship is expressed through connections, so it needs to be configured at the upper level
|
||||
if (this.configs?.getNodeParent) {
|
||||
return this.configs.getNodeParent(node);
|
||||
}
|
||||
let parent = node.document.originTree.getParent(node);
|
||||
|
||||
// If currentParent is Group, get the parent of parent
|
||||
while (parent?.flowNodeType === FlowNodeBaseType.GROUP) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
const nodeMeta = parent.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(parent);
|
||||
if (subCanvas?.isCanvas) {
|
||||
// Get real parent node by subCanvas Configuration
|
||||
return subCanvas.parentNode;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the children of a node are private and cannot be accessed by subsequent nodes.
|
||||
* @param node The node to check.
|
||||
* @returns `true` if the children are private, `false` otherwise.
|
||||
*/
|
||||
protected isNodeChildrenPrivate(node?: FlowNodeEntity): boolean {
|
||||
if (this.configs?.isNodeChildrenPrivate) {
|
||||
return node ? this.configs?.isNodeChildrenPrivate(node) : false;
|
||||
}
|
||||
|
||||
const isSystemNode = node?.id.startsWith('$');
|
||||
|
||||
// Except system node and group node, everything else is private
|
||||
return !isSystemNode && node?.flowNodeType !== FlowNodeBaseType.GROUP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts all scopes in the scope chain.
|
||||
* @returns An empty array, as this method is not implemented.
|
||||
*/
|
||||
sortAll(): Scope[] {
|
||||
// Not implemented yet
|
||||
console.warn('FreeLayoutScopeChain.sortAll is not implemented');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { BaseVariableField, VariableEngine } from '@flowgram.ai/variable-core';
|
||||
import { type ASTNode, ASTNodeJSON } from '@flowgram.ai/variable-core';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
import { EntityData } from '@flowgram.ai/core';
|
||||
|
||||
import { FlowNodeScope, FlowNodeScopeMeta, FlowNodeScopeTypeEnum } from './types';
|
||||
|
||||
interface Options {
|
||||
variableEngine: VariableEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages variable data for a flow node, including public and private scopes.
|
||||
*/
|
||||
export class FlowNodeVariableData extends EntityData {
|
||||
static type: string = 'FlowNodeVariableData';
|
||||
|
||||
declare entity: FlowNodeEntity;
|
||||
|
||||
readonly variableEngine: VariableEngine;
|
||||
|
||||
/**
|
||||
* Private variables can be accessed by public ones, but not the other way around.
|
||||
*/
|
||||
protected _private?: FlowNodeScope;
|
||||
|
||||
protected _public: FlowNodeScope;
|
||||
|
||||
/**
|
||||
* The private scope of the node.
|
||||
*/
|
||||
get private() {
|
||||
return this._private;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public scope of the node.
|
||||
*/
|
||||
get public() {
|
||||
return this._public;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a variable in the public AST (Abstract Syntax Tree) with the given key and JSON value.
|
||||
*
|
||||
* @param key - The key under which the variable will be stored.
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setVar(key: string, json: ASTNodeJSON): ASTNode;
|
||||
|
||||
/**
|
||||
* Sets a variable in the public AST (Abstract Syntax Tree) with the default key 'outputs'.
|
||||
*
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setVar(json: ASTNodeJSON): ASTNode;
|
||||
|
||||
public setVar(arg1: string | ASTNodeJSON, arg2?: ASTNodeJSON): ASTNode {
|
||||
if (typeof arg1 === 'string' && arg2 !== undefined) {
|
||||
return this.public.ast.set(arg1, arg2);
|
||||
}
|
||||
|
||||
if (typeof arg1 === 'object' && arg2 === undefined) {
|
||||
return this.public.ast.set('outputs', arg1);
|
||||
}
|
||||
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a variable from the public AST (Abstract Syntax Tree) by key.
|
||||
*
|
||||
* @param key - The key of the variable to retrieve. Defaults to 'outputs'.
|
||||
* @returns The value of the variable, or undefined if not found.
|
||||
*/
|
||||
public getVar(key: string = 'outputs') {
|
||||
return this.public.ast.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a variable from the public AST (Abstract Syntax Tree) by key.
|
||||
*
|
||||
* @param key - The key of the variable to clear. Defaults to 'outputs'.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public clearVar(key: string = 'outputs') {
|
||||
return this.public.ast.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a variable in the private AST (Abstract Syntax Tree) with the given key and JSON value.
|
||||
*
|
||||
* @param key - The key under which the variable will be stored.
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setPrivateVar(key: string, json: ASTNodeJSON): ASTNode;
|
||||
|
||||
/**
|
||||
* Sets a variable in the private AST (Abstract Syntax Tree) with the default key 'outputs'.
|
||||
*
|
||||
* @param json - The JSON value to store.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public setPrivateVar(json: ASTNodeJSON): ASTNode;
|
||||
|
||||
public setPrivateVar(arg1: string | ASTNodeJSON, arg2?: ASTNodeJSON): ASTNode {
|
||||
if (typeof arg1 === 'string' && arg2 !== undefined) {
|
||||
return this.initPrivate().ast.set(arg1, arg2);
|
||||
}
|
||||
|
||||
if (typeof arg1 === 'object' && arg2 === undefined) {
|
||||
return this.initPrivate().ast.set('outputs', arg1);
|
||||
}
|
||||
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a variable from the private AST (Abstract Syntax Tree) by key.
|
||||
*
|
||||
* @param key - The key of the variable to retrieve. Defaults to 'outputs'.
|
||||
* @returns The value of the variable, or undefined if not found.
|
||||
*/
|
||||
public getPrivateVar(key: string = 'outputs') {
|
||||
return this.private?.ast.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a variable from the private AST (Abstract Syntax Tree) by key.
|
||||
*
|
||||
* @param key - The key of the variable to clear. Defaults to 'outputs'.
|
||||
* @returns The updated AST node.
|
||||
*/
|
||||
public clearPrivateVar(key: string = 'outputs') {
|
||||
return this.private?.ast.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array containing all scopes (public and private) of the node.
|
||||
*/
|
||||
get allScopes(): FlowNodeScope[] {
|
||||
const res = [];
|
||||
|
||||
if (this._public) {
|
||||
res.push(this._public);
|
||||
}
|
||||
if (this._private) {
|
||||
res.push(this._private);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
getDefaultData() {
|
||||
return {};
|
||||
}
|
||||
|
||||
constructor(entity: FlowNodeEntity, readonly opts: Options) {
|
||||
super(entity);
|
||||
|
||||
const { variableEngine } = opts || {};
|
||||
this.variableEngine = variableEngine;
|
||||
this._public = this.variableEngine.createScope(this.entity.id, {
|
||||
node: this.entity,
|
||||
type: FlowNodeScopeTypeEnum.public,
|
||||
} as FlowNodeScopeMeta);
|
||||
this.toDispose.push(this._public);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns the private scope for the node.
|
||||
* If the private scope already exists, it returns the existing one.
|
||||
* @returns The private scope of the node.
|
||||
*/
|
||||
initPrivate(): FlowNodeScope {
|
||||
if (!this._private) {
|
||||
this._private = this.variableEngine.createScope(`${this.entity.id}_private`, {
|
||||
node: this.entity,
|
||||
type: FlowNodeScopeTypeEnum.private,
|
||||
} as FlowNodeScopeMeta);
|
||||
|
||||
this.variableEngine.chain.refreshAllChange();
|
||||
|
||||
this.toDispose.push(this._private);
|
||||
}
|
||||
return this._private;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable field by key path in the public scope by scope chain.
|
||||
* @param keyPath - The key path of the variable field.
|
||||
* @returns The variable field, or undefined if not found.
|
||||
*/
|
||||
getByKeyPath(keyPath: string[]): BaseVariableField | undefined {
|
||||
return this.public.available.getByKeyPath(keyPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable field by key path in the private scope by scope chain.
|
||||
* @param keyPath - The key path of the variable field.
|
||||
* @returns The variable field, or undefined if not found.
|
||||
*/
|
||||
getByKeyPathInPrivate(keyPath: string[]): BaseVariableField | undefined {
|
||||
return this.private?.available.getByKeyPath(keyPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { FlowNodeVariableData } from './flow-node-variable-data';
|
||||
export { FreeLayoutScopeChain } from './chains/free-layout-scope-chain';
|
||||
export { VariableChainConfig } from './variable-chain-config';
|
||||
export { FixedLayoutScopeChain } from './chains/fixed-layout-scope-chain';
|
||||
export {
|
||||
type FlowNodeScopeMeta,
|
||||
type FlowNodeScope,
|
||||
FlowNodeScopeTypeEnum as FlowNodeScopeType,
|
||||
} from './types';
|
||||
export { GlobalScope, bindGlobalScope } from './scopes/global-scope';
|
||||
export { ScopeChainTransformService } from './services/scope-chain-transform-service';
|
||||
export { getNodeScope, getNodePrivateScope } from './utils';
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { injectable, interfaces } from 'inversify';
|
||||
import { Scope, VariableEngine } from '@flowgram.ai/variable-core';
|
||||
|
||||
/**
|
||||
* Global Scope stores all variables that are not scoped to any node.
|
||||
*
|
||||
* - Variables in Global Scope can be accessed by any node.
|
||||
* - Any other scope's variables can not be accessed by Global Scope.
|
||||
*/
|
||||
@injectable()
|
||||
export class GlobalScope extends Scope {
|
||||
static readonly ID = Symbol('GlobalScope');
|
||||
|
||||
/**
|
||||
* Check if the scope is Global Scope.
|
||||
* @param scope
|
||||
* @returns
|
||||
*/
|
||||
static is(scope: Scope) {
|
||||
return scope.id === GlobalScope.ID;
|
||||
}
|
||||
}
|
||||
|
||||
export const bindGlobalScope = (bind: interfaces.Bind) => {
|
||||
bind(GlobalScope).toDynamicValue((ctx) => {
|
||||
const variableEngine = ctx.container.get(VariableEngine);
|
||||
let scope = variableEngine.getScopeById(GlobalScope.ID) as GlobalScope;
|
||||
|
||||
if (!scope) {
|
||||
scope = variableEngine.createScope(
|
||||
GlobalScope.ID,
|
||||
{},
|
||||
{ ScopeConstructor: GlobalScope }
|
||||
) as GlobalScope;
|
||||
variableEngine.chain.refreshAllChange();
|
||||
}
|
||||
|
||||
return scope;
|
||||
});
|
||||
};
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { inject, injectable, optional } from 'inversify';
|
||||
import { Scope, VariableEngine } from '@flowgram.ai/variable-core';
|
||||
import { FlowDocument } from '@flowgram.ai/document';
|
||||
import { lazyInject } from '@flowgram.ai/core';
|
||||
|
||||
import { VariableChainConfig } from '../variable-chain-config';
|
||||
import { FlowNodeScope } from '../types';
|
||||
|
||||
/**
|
||||
* Context for scope transformers.
|
||||
*/
|
||||
export interface TransformerContext {
|
||||
/**
|
||||
* The current scope
|
||||
*/
|
||||
scope: FlowNodeScope;
|
||||
/**
|
||||
* The flow document.
|
||||
*/
|
||||
document: FlowDocument;
|
||||
/**
|
||||
* The variable engine.
|
||||
*/
|
||||
variableEngine: VariableEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that transforms an array of scopes.
|
||||
* @param scopes The array of scopes to transform.
|
||||
* @param ctx The transformer context.
|
||||
* @returns The transformed array of scopes.
|
||||
*/
|
||||
export type IScopeTransformer = (scopes: Scope[], ctx: TransformerContext) => Scope[];
|
||||
|
||||
const passthrough: IScopeTransformer = (scopes, ctx) => scopes;
|
||||
|
||||
/**
|
||||
* A service for transforming scope chains.
|
||||
*/
|
||||
@injectable()
|
||||
export class ScopeChainTransformService {
|
||||
protected transformerMap: Map<
|
||||
string,
|
||||
{ transformDeps: IScopeTransformer; transformCovers: IScopeTransformer }
|
||||
> = new Map();
|
||||
|
||||
@lazyInject(FlowDocument) document: FlowDocument;
|
||||
|
||||
@lazyInject(VariableEngine) variableEngine: VariableEngine;
|
||||
|
||||
constructor(
|
||||
@optional()
|
||||
@inject(VariableChainConfig)
|
||||
protected configs?: VariableChainConfig
|
||||
) {
|
||||
if (this.configs?.transformDeps || this.configs?.transformCovers) {
|
||||
this.transformerMap.set('VARIABLE_LAYOUT_CONFIG', {
|
||||
transformDeps: this.configs.transformDeps || passthrough,
|
||||
transformCovers: this.configs.transformCovers || passthrough,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check if transformer registered
|
||||
* @param transformerId used to identify transformer, prevent duplicated
|
||||
* @returns
|
||||
*/
|
||||
hasTransformer(transformerId: string) {
|
||||
return this.transformerMap.has(transformerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* register new transform function
|
||||
* @param transformerId used to identify transformer, prevent duplicated transformer
|
||||
* @param transformer The transformer to register.
|
||||
*/
|
||||
registerTransformer(
|
||||
transformerId: string,
|
||||
transformer: {
|
||||
transformDeps: IScopeTransformer;
|
||||
transformCovers: IScopeTransformer;
|
||||
}
|
||||
) {
|
||||
this.transformerMap.set(transformerId, transformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the dependency scopes.
|
||||
* @param scopes The array of scopes to transform.
|
||||
* @param param1 The context for the transformation.
|
||||
* @returns The transformed array of scopes.
|
||||
*/
|
||||
transformDeps(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
||||
return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {
|
||||
if (!transformer.transformDeps) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
scopes = transformer.transformDeps(scopes, {
|
||||
scope,
|
||||
document: this.document,
|
||||
variableEngine: this.variableEngine,
|
||||
});
|
||||
return scopes;
|
||||
}, scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the cover scopes.
|
||||
* @param scopes The array of scopes to transform.
|
||||
* @param param1 The context for the transformation.
|
||||
* @returns The transformed array of scopes.
|
||||
*/
|
||||
transformCovers(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {
|
||||
return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {
|
||||
if (!transformer.transformCovers) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
scopes = transformer.transformCovers(scopes, {
|
||||
scope,
|
||||
document: this.document,
|
||||
variableEngine: this.variableEngine,
|
||||
});
|
||||
return scopes;
|
||||
}, scopes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Scope } from '@flowgram.ai/variable-core';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
/**
|
||||
* Enum for flow node scope types.
|
||||
*/
|
||||
export enum FlowNodeScopeTypeEnum {
|
||||
/**
|
||||
* Public scope.
|
||||
*/
|
||||
public = 'public',
|
||||
/**
|
||||
* Private scope.
|
||||
*/
|
||||
private = 'private',
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for a flow node scope.
|
||||
*/
|
||||
export interface FlowNodeScopeMeta {
|
||||
/**
|
||||
* The flow node entity associated with the scope.
|
||||
*/
|
||||
node?: FlowNodeEntity;
|
||||
/**
|
||||
* The type of the scope.
|
||||
*/
|
||||
type?: FlowNodeScopeTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a virtual node in the scope chain.
|
||||
*/
|
||||
export interface ScopeVirtualNode {
|
||||
/**
|
||||
* The ID of the virtual node.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The type of the flow node.
|
||||
*/
|
||||
flowNodeType: 'virtualNode';
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a node in the scope chain, which can be either a flow node entity or a virtual node.
|
||||
*/
|
||||
export type ScopeChainNode = FlowNodeEntity | ScopeVirtualNode;
|
||||
|
||||
/**
|
||||
* Represents a scope associated with a flow node.
|
||||
*/
|
||||
export interface FlowNodeScope extends Scope<FlowNodeScopeMeta> {}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { FlowNodeVariableData } from './flow-node-variable-data';
|
||||
|
||||
/**
|
||||
* Use `node.scope` instead.
|
||||
* @deprecated
|
||||
* @param node The flow node entity.
|
||||
* @returns The public scope of the node.
|
||||
*/
|
||||
export function getNodeScope(node: FlowNodeEntity) {
|
||||
return node.getData(FlowNodeVariableData).public;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `node.privateScope` instead.
|
||||
* @deprecated
|
||||
* @param node The flow node entity.
|
||||
* @returns The private scope of the node.
|
||||
*/
|
||||
export function getNodePrivateScope(node: FlowNodeEntity) {
|
||||
return node.getData(FlowNodeVariableData).initPrivate();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { FlowNodeEntity } from '@flowgram.ai/document';
|
||||
|
||||
import { type ScopeChainNode } from './types';
|
||||
import { IScopeTransformer } from './services/scope-chain-transform-service';
|
||||
|
||||
/**
|
||||
* Configuration for the variable chain.
|
||||
*/
|
||||
export interface VariableChainConfig {
|
||||
/**
|
||||
* The output variables of a node's children cannot be accessed by subsequent nodes.
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
isNodeChildrenPrivate?: (node: ScopeChainNode) => boolean;
|
||||
|
||||
/**
|
||||
* For fixed layout scenarios: there are a large number of useless nodes between parent and child (such as inlineBlocks, etc., which need to be configured to be skipped)
|
||||
* For free canvas scenarios: in some scenarios, the parent-child relationship between nodes is expressed through connections or other interactive forms, which needs to be configurable
|
||||
*/
|
||||
getNodeChildren?: (node: FlowNodeEntity) => FlowNodeEntity[];
|
||||
getNodeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;
|
||||
|
||||
/**
|
||||
* Fine-tune the dependency scope.
|
||||
*/
|
||||
transformDeps?: IScopeTransformer;
|
||||
|
||||
/**
|
||||
* Fine-tune the cover scope.
|
||||
*/
|
||||
transformCovers?: IScopeTransformer;
|
||||
}
|
||||
|
||||
export const VariableChainConfig = Symbol('VariableChainConfig');
|
||||
Reference in New Issue
Block a user