join-status.js 2.98 KB
/**
 * fan-in 等待态(27-P4 T10):一个 pending 节点在等哪几个上游、各自是什么状态。
 *
 * 并行(27-P4 T4/T6)落地之后一张图可能同时有好几个分支在跑,「这个节点为什么还没轮到」
 * 不再是一眼能看出来的事——以前只有一条链,pending 的下一个必然是紧邻上游那一个;
 * 现在它可能在等三个并行分支里最慢的那一个。§4.3 决策要求「run 层面要能回答『我卡在等
 * 哪几个节点』」,这里就是那句话的数据源。
 *
 * 判据与引擎 `graph-engine.activateSuccessTargets`/`allIncomingSatisfied` 同源:
 * 只看 forward 的 success 入边——backward 边是异常流程,不参与「汇合点」这道闸,
 * fail 边同理。跟 io-model.js 的 inboundEdges 用的是同一条筛选条件,别改出分叉。
 */

/**
 * @param {object} node   getRunState 的合并节点(要 status 与 defNodeId 两个键)
 * @param {Array}  nodes  同图全部节点(拿名字与上游状态用)
 * @param {Array}  edges  同图全部边
 * @returns {null | {
 *   waiting: Array<{defNodeId, name, status}>,
 *   total: number, done: number, satisfied: boolean,
 *   isFanIn: boolean,   // 2+ 条 forward success 入边才算真正的 fan-in;单一入边只是普通顺序等待
 *   blockedBy: Array<{defNodeId, name, status}>,
 * }}
 */
export function resolveJoinWait(node, nodes = [], edges = []) {
  if (!node || node.status !== 'pending') return null;
  const incoming = (edges || []).filter((e) => (
    String(e.to) === String(node.defNodeId)
    && (e.condition || 'success') === 'success'
    && (e.edgeKind || 'forward') !== 'backward'
  ));
  if (!incoming.length) return null; // 没有 forward success 入边:不是在等 fan-in,是别的原因还没轮到

  const byId = new Map((nodes || []).map((n) => [String(n.defNodeId), n]));
  const waiting = incoming.map((e) => {
    const src = byId.get(String(e.from));
    return {
      defNodeId: e.from,
      name: (src && (src.name || src.defNodeId)) || e.from,
      status: (src && src.status) || 'pending',
    };
  });

  const done = waiting.filter((w) => w.status === 'success').length;
  return {
    waiting,
    total: waiting.length,
    done,
    satisfied: done === waiting.length,
    isFanIn: waiting.length > 1,
    blockedBy: waiting.filter((w) => w.status !== 'success'),
  };
}

/**
 * 画布上的紧凑摘要——克制原则同 `edgeContractLabel`:只占一个小徽标的位置,
 * 完整明细留给 title。`satisfied` 时不显示(该节点这一刻就会被点亮,不算「在等」);
 * 非 fan-in(只有一条入边)时也不显示——普通顺序等待,看紧邻上游的状态就够了,
 * 加一个「1/1」徽标只会制造噪声。
 */
export function joinWaitLabel(wait) {
  if (!wait || !wait.isFanIn || wait.satisfied) return null;
  return {
    text: `⏳${wait.done}/${wait.total}`,
    title: `在等:${wait.waiting.map((w) => `${w.name}(${w.status})`).join(' · ')}`,
  };
}