feat(web): live SSE updates in the dependency-graph drawer (TASK-1785) - #722
Merged
Conversation
Subscribe ItemGraph to the workspace SSE stream while open so the graph reflects changes live: - Correlate events to visible nodes by item UUID (added id to RenderNode). - item_updated: glow the node + debounced BACKGROUND refetch (no spinner, no view-refit) to pick up status/title/terminal changes. - item_created/archived/restored: debounced refetch (structural). - comment_created: glow only (ambient liveness). - onSyncRequired (bulk updates / replay-gap backfills): folded into the same debounced refetch. Refactor the load path into load(background): background mode keeps the last good graph visible and swallows errors so an ambient blip never replaces a working view with an error card. Transient node glow auto-clears after 2.5s; a small "updating…" indicator shows during a background reload. Subscription is set up in onMount and torn down on close (drawer-scoped). Parent: PLAN-1780.
…down per Codex review (round 1) - handleItemEvent now no-ops unless the event's item is in the current neighborhood, so unrelated workspace edits no longer trigger background refetches against api.graph.getFocused. A newly-linked neighbor still surfaces: linking touches the visible endpoint (item_updated, in-view) and that refetches. item_created is ignored (never already in view). - Bump loadToken in the onMount teardown so an in-flight load can't commit $state or queue fitView after the drawer closes / component unmounts. Parent: PLAN-1780.
…nd 2) The per-node glow setTimeout could fire after unmount and write touchedRefs on a destroyed component. Track the handles and cancel them in teardown, matching the refetchTimer/loadToken teardown discipline. Parent: PLAN-1780.
…round 3) touch() now resets a per-ref fade timer instead of stacking timeouts, so a burst of events for the same node keeps it lit and the fade starts GLOW_MS after the LAST event. Replaced the one-shot keyframe with a transition-based glow (filter transition on .node-bg) so rapid re-touches don't fight an animation that can't restart — the node stays lit while touched and fades out when the ref leaves touchedRefs. Kept onSyncRequired → debounced refetch (round 3 finding 2): it's the staleness-recovery net for bulk-update and replay-gap-reconnect events, which are rare and carry no item_id to filter on. The 400ms debounce collapses bursts to a single background reload, so it's not a per-edit refresh path. Parent: PLAN-1780.
…ex review (round 4) scheduleRefetch() now no-ops unless loadState === 'ready'. Previously an onSyncRequired/SSE event during the initial foreground load could start a background load(true) that bumped loadToken (cancelling the foreground load) while skipping fit + error handling — leaving the view un-fitted, or stuck on the spinner forever if that background fetch failed. The in-flight initial load already fetches the latest data, so dropping events before the first ready render loses nothing. Parent: PLAN-1780.
…review (round 5) The loadState !== 'ready' guard prevented the spinner/fit race but dropped the recovery signal for mutations arriving mid-initial-load: if a change lands after the initial request is issued, that response can be stale and no later event would correct it. Now such requests set a pendingRefetch flag that load() flushes once it commits a ready graph, so the neighborhood always reconciles. Parent: PLAN-1780.
…use) per Codex review Close the load/refetch race class generally rather than per-path: - A foreground load (initial / reroot / depth / include-done) now clears any armed background refetch timer at start, so the delayed callback can't fire later, bump loadToken, and cancel the foreground load (which on a background failure left the view stuck on the spinner). - The background timer callback re-checks loadState === 'ready' at fire time and defers (pendingRefetch) instead of clobbering if state has moved on. Parent: PLAN-1780.
Reconcile the refetch-breadth tradeoff in favor of correctness. The server publishes structural link changes (new child, reparent) on the CHILD — which may be outside the current neighborhood — and the SSE payload carries no link info, so gating refetch on in-view items missed newly-attached/reparented neighbors. Refetch on any item_updated/created/archived/restored instead; glow stays in-view-only (off-view items have no node to light up). Cost is bounded by the 400ms debounce (one small getFocused per burst) and the drawer's transient lifetime. Parent: PLAN-1780.
… review (round 8) Two concurrency fixes for the SSE refetch path: - Coalesce: a loadInFlight guard prevents starting a new background load while one is awaiting. Previously a steady event stream (spaced > debounce but faster than getFocused+layout) kept bumping loadToken and invalidating in-flight responses, so live updates could starve forever. Now at most one load runs at a time; deferred events flush (exactly once) when it settles. - Error recovery: refetch decisions moved into runRefetch(), which retries foreground from the 'error' state on the next live event. Previously events during/after a failed load only set pendingRefetch (flushed solely on a successful ready), so the graph could stay stuck in error until a manual retry. pendingRefetch is now flushed from load()'s finally on every outcome. Parent: PLAN-1780.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the per-item dependency graph drawer live: while it's open,
ItemGraphsubscribes to the workspace SSE stream and reflects changes as they happen.idto the rendered node model).item_updated→ glow the node + debounced background refetch (no spinner, no view-refit) to pick up status/title/terminal changes.item_created/item_archived/item_restored→ debounced refetch (structural; a node may appear/disappear).comment_created→ glow only (ambient liveness).onSyncRequired(bulk updates / replay-gap backfills) → folded into the same debounced refetch.The load path was refactored into
load(background): background mode keeps the last good graph visible and swallows errors, so an ambient blip never replaces a working view with an error card. Transient node glow auto-clears after 2.5s; a small "updating…" indicator shows during a background reload. The subscription is set up inonMountand torn down on close (drawer-scoped — no listeners when closed).Context
Implements
TASK-1785underPLAN-1780. Builds on the drawer (#721). Reuses the existingsseServicethe workspace layout already connects.Test plan