chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
@@ -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;
});
};
@@ -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');