fix: don't re-pulse already-running jobs after they finish

The catch-up pulse re-added a completed job to the active set if its
start was within the (lagging) lookback window — even one we'd already
animated the whole time it ran — keeping its edges lit ~a poll
interval past completion (~5s after a 3.5s test). Track job ids seen
in-flight and skip the pulse for them; it still fires for hops whose
whole lifetime fell between two polls. Bound the set in lockstep with
eventsById; cleared on dispose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-05-16 12:32:13 +00:00
parent 40decb5467
commit 0f19d59183
@@ -94,6 +94,11 @@ export function useActiveRunnableIds(
{ runs: number; lastStatus: RunStatus; lastTs: string }
>()
const countedJobIds = new Set<string>()
// Job ids we've observed in-flight at least once. The catch-up pulse is
// only for jobs whose whole start→finish fell between two polls (never
// seen running); re-pulsing a job we already animated while it ran just
// keeps its edges lit ~one extra poll interval after it finished.
const seenInFlightJobIds = new Set<string>()
// jobId → latest known state of that job, for the activity log. Survives
// `stop()` (log keeps history while idle); cleared by `dispose()`.
const eventsById = new Map<string, PipelineEvent>()
@@ -144,10 +149,15 @@ export function useActiveRunnableIds(
next.add(id)
inFlightThisTick.add(id)
anyInFlight = true
if (jobId) seenInFlightJobIds.add(jobId)
} else {
// completed: catch-up — a hop that started after the last
// poll (and already finished) still gets one pulse.
if (startedTs && startedTs >= since) next.add(id)
// completed: catch-up — a hop whose whole lifetime fell
// between two polls (never observed in-flight) still gets
// one pulse. Jobs we already animated while running are
// NOT re-pulsed, else their edges linger ~a poll interval
// past completion.
if (startedTs && startedTs >= since && (!jobId || !seenInFlightJobIds.has(jobId)))
next.add(id)
// Tally distinct completed jobs for the node badge — only
// those since the graph was opened (older = pre-existing
// history). Jobs come newest-first; only a strictly newer
@@ -208,7 +218,11 @@ export function useActiveRunnableIds(
if (sorted.length > MAX_EVENTS * 4) {
for (const e of sorted.slice(MAX_EVENTS * 4)) eventsById.delete(e.id)
countedJobIds.clear()
for (const id of eventsById.keys()) countedJobIds.add(id)
seenInFlightJobIds.clear()
for (const id of eventsById.keys()) {
countedJobIds.add(id)
seenInFlightJobIds.add(id)
}
}
const nextEvents = sorted.slice(0, MAX_EVENTS)
if (!eventsEq(events, nextEvents)) events = nextEvents
@@ -287,6 +301,7 @@ export function useActiveRunnableIds(
stop()
completedHistory.clear()
countedJobIds.clear()
seenInFlightJobIds.clear()
eventsById.clear()
if (states.size > 0) states = new Map()
if (events.length > 0) events = []