156 lines
6.0 KiB
TypeScript
156 lines
6.0 KiB
TypeScript
/**
|
|
* Hook that holds all connection-path state and callbacks for the canvas.
|
|
* Used to show the "ant trail" along the path of an update (e.g. config → agent → render).
|
|
* Extracted from CanvasPage to keep the page focused on composition.
|
|
*/
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { getPathNodeIds, getPathToUpdatingSegmentNodeIds } from '@/lib/graph/graphPath'
|
|
|
|
/** Serialize set to a stable string for equality. */
|
|
function setToStableKey(s: Set<string>): string {
|
|
return Array.from(s).sort().join(',')
|
|
}
|
|
|
|
export type EdgeLike = { source: string; target: string }
|
|
|
|
const PULSE_MS = 1500
|
|
|
|
export type UseCanvasConnectionPathResult = {
|
|
connectionPathUpdatingNodeIds: string[]
|
|
connectionPathTriggerNodeIds: string[]
|
|
connectionPathPausedNodeIds: string[]
|
|
connectionPathErrorNodeIds: string[]
|
|
connectionPathNodeIds: Set<string>
|
|
connectionPathPausedSegmentNodeIds: Set<string>
|
|
connectionPathActiveSegmentNodeIds: Set<string>
|
|
startConnectionPathUpdate: (nodeId: string) => void
|
|
endConnectionPathUpdate: (nodeId: string) => void
|
|
addConnectionPathTrigger: (nodeId: string) => void
|
|
addConnectionPathPausedNode: (nodeId: string) => void
|
|
removeConnectionPathPausedNode: (nodeId: string) => void
|
|
addConnectionPathError: (nodeId: string) => void
|
|
removeConnectionPathError: (nodeId: string) => void
|
|
}
|
|
|
|
export function useCanvasConnectionPath(edges: EdgeLike[]): UseCanvasConnectionPathResult {
|
|
const [connectionPathTriggerNodeIds, setConnectionPathTriggerNodeIds] = useState<string[]>([])
|
|
const [pulseEndsAt, setPulseEndsAt] = useState<number | null>(null)
|
|
const [connectionPathErrorNodeIds, setConnectionPathErrorNodeIds] = useState<string[]>([])
|
|
|
|
const pathTriggerBatchRef = useRef<Set<string>>(new Set())
|
|
const pathTriggerScheduledRef = useRef(false)
|
|
const pulseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
const addConnectionPathTrigger = useCallback((nodeId: string) => {
|
|
pathTriggerBatchRef.current.add(nodeId)
|
|
if (pathTriggerScheduledRef.current) return
|
|
pathTriggerScheduledRef.current = true
|
|
requestAnimationFrame(() => {
|
|
pathTriggerScheduledRef.current = false
|
|
const batch = new Set(pathTriggerBatchRef.current)
|
|
pathTriggerBatchRef.current = new Set()
|
|
if (batch.size === 0) return
|
|
setConnectionPathTriggerNodeIds((prev) => {
|
|
const next = new Set(prev)
|
|
batch.forEach((id) => next.add(id))
|
|
return next.size === prev.length && prev.every((id) => next.has(id)) ? prev : Array.from(next)
|
|
})
|
|
setPulseEndsAt(Date.now() + PULSE_MS)
|
|
})
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (pulseEndsAt == null) return
|
|
const delay = Math.max(0, pulseEndsAt - Date.now())
|
|
if (pulseTimerRef.current != null) clearTimeout(pulseTimerRef.current)
|
|
pulseTimerRef.current = setTimeout(() => {
|
|
pulseTimerRef.current = null
|
|
setPulseEndsAt(null)
|
|
setConnectionPathTriggerNodeIds([])
|
|
}, delay)
|
|
return () => {
|
|
if (pulseTimerRef.current != null) {
|
|
clearTimeout(pulseTimerRef.current)
|
|
pulseTimerRef.current = null
|
|
}
|
|
}
|
|
}, [pulseEndsAt])
|
|
|
|
const connectionPathNodeIdsRaw = useMemo(
|
|
() =>
|
|
getPathNodeIds(edges, [], connectionPathTriggerNodeIds, undefined),
|
|
[edges, connectionPathTriggerNodeIds]
|
|
)
|
|
const connectionPathNodeIdsRef = useRef<Set<string>>(connectionPathNodeIdsRaw)
|
|
const connectionPathNodeIdsKeyRef = useRef<string>('')
|
|
const connectionPathNodeIds =
|
|
setToStableKey(connectionPathNodeIdsRaw) === connectionPathNodeIdsKeyRef.current
|
|
? connectionPathNodeIdsRef.current
|
|
: (() => {
|
|
connectionPathNodeIdsKeyRef.current = setToStableKey(connectionPathNodeIdsRaw)
|
|
connectionPathNodeIdsRef.current = connectionPathNodeIdsRaw
|
|
return connectionPathNodeIdsRaw
|
|
})()
|
|
|
|
const EMPTY_PAUSED_SEGMENT = useMemo(() => new Set<string>(), [])
|
|
const connectionPathPausedSegmentNodeIds = EMPTY_PAUSED_SEGMENT
|
|
|
|
const pulseActive = pulseEndsAt != null
|
|
const connectionPathActiveSegmentNodeIdsRaw = useMemo(
|
|
() =>
|
|
getPathToUpdatingSegmentNodeIds(
|
|
edges,
|
|
connectionPathTriggerNodeIds,
|
|
pulseActive
|
|
),
|
|
[edges, connectionPathTriggerNodeIds, pulseActive]
|
|
)
|
|
const connectionPathActiveSegmentNodeIdsRef = useRef<Set<string>>(
|
|
connectionPathActiveSegmentNodeIdsRaw
|
|
)
|
|
const connectionPathActiveSegmentNodeIdsKeyRef = useRef<string>('')
|
|
const connectionPathActiveSegmentNodeIds =
|
|
setToStableKey(connectionPathActiveSegmentNodeIdsRaw) ===
|
|
connectionPathActiveSegmentNodeIdsKeyRef.current
|
|
? connectionPathActiveSegmentNodeIdsRef.current
|
|
: (() => {
|
|
connectionPathActiveSegmentNodeIdsKeyRef.current = setToStableKey(
|
|
connectionPathActiveSegmentNodeIdsRaw
|
|
)
|
|
connectionPathActiveSegmentNodeIdsRef.current = connectionPathActiveSegmentNodeIdsRaw
|
|
return connectionPathActiveSegmentNodeIdsRaw
|
|
})()
|
|
|
|
const addConnectionPathPausedNode = useCallback((_nodeId: string) => {}, [])
|
|
const removeConnectionPathPausedNode = useCallback((_nodeId: string) => {}, [])
|
|
|
|
const startConnectionPathUpdate = useCallback((_nodeId: string) => {}, [])
|
|
const endConnectionPathUpdate = useCallback((_nodeId: string) => {}, [])
|
|
|
|
const addConnectionPathError = useCallback((nodeId: string) => {
|
|
setConnectionPathErrorNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
|
}, [])
|
|
|
|
const removeConnectionPathError = useCallback((nodeId: string) => {
|
|
setConnectionPathErrorNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
|
}, [])
|
|
|
|
return {
|
|
connectionPathUpdatingNodeIds: [],
|
|
connectionPathTriggerNodeIds,
|
|
connectionPathPausedNodeIds: [],
|
|
connectionPathErrorNodeIds,
|
|
connectionPathNodeIds,
|
|
connectionPathPausedSegmentNodeIds,
|
|
connectionPathActiveSegmentNodeIds,
|
|
startConnectionPathUpdate,
|
|
endConnectionPathUpdate,
|
|
addConnectionPathTrigger,
|
|
addConnectionPathPausedNode,
|
|
removeConnectionPathPausedNode,
|
|
addConnectionPathError,
|
|
removeConnectionPathError,
|
|
}
|
|
}
|