80 lines
6.8 KiB
TypeScript
80 lines
6.8 KiB
TypeScript
/**
|
|
* 旧跨board子图的父链与事务范围规划。共同祖先拥有跨分区连线,端点逐层通过分区端口向上暴露。
|
|
* 这里只计算读写参与者、路径与请求描述,不创建元素/端口或发网络;调用方负责加载足够的文档。
|
|
*/
|
|
import type { CanvasDocument } from "./canvas.ts";
|
|
/** 从入口节点建立子board→父board/frame索引;重复子ID后项覆盖,不替代单父归属的独立校验。 */
|
|
export function subgraphParents(documents: Map<string, CanvasDocument>) {
|
|
const parents = new Map<string, { boardId: string; frameId: string }>();
|
|
for (const [boardId, document] of documents) for (const node of document.nodes) if (node.data.subgraph) parents.set(node.data.subgraph.boardId, { boardId, frameId: node.id });
|
|
return parents;
|
|
}
|
|
/** 移动至少涉及parent/source/target,选中入口还纳入其后代;祖先端口引用受影响时再纳入上一层owner。 */
|
|
export function subgraphMoveParticipants(documents: Map<string, CanvasDocument>, parent: string, source: string, target: string, ids: string[]) {
|
|
const participants = new Set([parent, source, target]);
|
|
const parents = subgraphParents(documents);
|
|
/** 递归收集选中入口后代,重复访问报错,防循环或重复树引用被误当作独立分支。 */
|
|
const addDescendants = (board: string, seen = new Set<string>()) => {
|
|
if (seen.has(board)) throw new Error("subgraph_cycle"); seen.add(board); participants.add(board);
|
|
for (const node of documents.get(board)?.nodes || []) if (node.data.subgraph) addDescendants(node.data.subgraph.boardId, seen);
|
|
};
|
|
for (const node of documents.get(source)?.nodes || []) if (ids.includes(node.id) && node.data.subgraph) addDescendants(node.data.subgraph.boardId);
|
|
// 祖先端口影响只检查 parent 的直接上一层及其一层内端口映射,不是无限上溯的依赖闭包。
|
|
const ancestor = parents.get(parent);
|
|
if (ancestor) {
|
|
const frame = documents.get(ancestor.boardId)?.nodes.find((node) => node.id === ancestor.frameId);
|
|
const affected = frame?.data.ports?.some((port) => {
|
|
if (source === parent) return ids.includes(port.nodeId);
|
|
const inner = documents.get(parent)?.nodes.find((node) => node.id === port.nodeId);
|
|
return inner?.data.subgraph?.boardId === source && inner.data.ports?.some((p) => `port:${p.id}` === port.handle && p.direction === port.direction && ids.includes(p.nodeId));
|
|
});
|
|
if (affected) participants.add(ancestor.boardId);
|
|
}
|
|
return [...participants];
|
|
}
|
|
/** 沿source上行到最近共同祖先,再下行到target,返回逐层父子移动描述;无共同根拒绝。 */
|
|
export function subgraphMovePath(documents: Map<string, CanvasDocument>, source: string, target: string) {
|
|
const parents = subgraphParents(documents);
|
|
/** 包含自身的父链,重复board立即拒绝,避免循环遍历。 */
|
|
const chain = (board: string) => { const result = [board]; while (parents.has(board)) { board = parents.get(board)!.boardId; if (result.includes(board)) throw new Error("subgraph_cycle"); result.push(board); } return result; };
|
|
// 相同 source/target 得到空路径;父索引只反映已加载 documents,缺数据不能据此认定全局无共同根。
|
|
const from = chain(source), to = chain(target), common = from.find((id) => to.includes(id));
|
|
if (!common) throw new Error("ownership_conflict");
|
|
const path: { parent: string; frame: string; source: string; target: string }[] = [];
|
|
for (let i = 0; from[i] !== common; i++) { const relation = parents.get(from[i])!; path.push({ parent: relation.boardId, frame: relation.frameId, source: from[i], target: relation.boardId }); }
|
|
for (let i = to.indexOf(common) - 1; i >= 0; i--) { const relation = parents.get(to[i])!; path.push({ parent: relation.boardId, frame: relation.frameId, source: relation.boardId, target: to[i] }); }
|
|
return path;
|
|
}
|
|
/** 计算连线共同owner及其可引用的两端;每跨一层必须已有同方向匹配端口,不在此暗自补端口。 */
|
|
export function exposedConnection(documents: Map<string, CanvasDocument>, source: { boardId: string; id: string; handle?: string | null }, target: { boardId: string; id: string; handle?: string | null }) {
|
|
const parents = subgraphParents(documents);
|
|
/** 生成自身到根的链,最先共同项就是当前source/target的最近共同祖先。 */
|
|
const ancestors = (id: string) => { const result = [id]; while (parents.has(id)) { id = parents.get(id)!.boardId; if (result.includes(id)) throw new Error("subgraph_cycle"); result.push(id); } return result; };
|
|
const owner = ancestors(source.boardId).find((id) => ancestors(target.boardId).includes(id));
|
|
if (!owner) throw new Error("ownership_conflict");
|
|
/** 从内部node/handle逐层换成入口frame/port:id,保持源output、目标input方向。 */
|
|
const expose = (endpoint: typeof source, direction: "input" | "output") => {
|
|
let { boardId, id, handle } = endpoint;
|
|
while (boardId !== owner) {
|
|
const parent = parents.get(boardId)!;
|
|
const frame = documents.get(parent.boardId)?.nodes.find((node) => node.id === parent.frameId);
|
|
// handle 按 === 精确匹配,包括 undefined 与 null 的差别;不会在这里规范化端口表示。
|
|
const port = frame?.data.ports?.find((port) => port.nodeId === id && port.handle === handle && port.direction === direction);
|
|
if (!port) throw new Error("boundary_port_required");
|
|
id = parent.frameId; handle = `port:${port.id}`; boardId = parent.boardId;
|
|
}
|
|
return { id, handle };
|
|
};
|
|
return { boardId: owner, source: expose(source, "output"), target: expose(target, "input") };
|
|
}
|
|
/** 给服务端连接事务的原始端点描述与完整父链参与集;真正创建端口/边由事务执行层处理。 */
|
|
export function connectionTransactionPlan(documents: Map<string, CanvasDocument>, source: { boardId: string; id: string; handle: string }, target: { boardId: string; id: string; handle: string }) {
|
|
const parents = subgraphParents(documents);
|
|
/** 与其它父链查询一致地防循环,不因两点在不同子board就当成无共同owner。 */
|
|
const path = (id: string) => { const result = [id]; while (parents.has(id)) { id = parents.get(id)!.boardId; if (result.includes(id)) throw new Error("subgraph_cycle"); result.push(id); } return result; };
|
|
const from = path(source.boardId), to = path(target.boardId), owner = from.find((id) => to.includes(id));
|
|
if (!owner) throw new Error("ownership_conflict");
|
|
// 参与集按 source→owner、target→owner 首次出现顺序去重;这只是规划,不带服务版本或原子提交证明。
|
|
return { boardId: owner, participants: [...new Set([...from.slice(0, from.indexOf(owner) + 1), ...to.slice(0, to.indexOf(owner) + 1)])], source: { board_id: source.boardId, node_id: source.id, handle: source.handle }, target: { board_id: target.boardId, node_id: target.id, handle: target.handle } };
|
|
}
|