feat(ui): live refresh and drift banners — the viewer keeps up with the project (CG-53)
`GET /api/events` is a server-sent-event stream the viewer holds open for the life of the page. Two signals, two things the browser could not know: changed source files touched on disk, before any sync — the drift banner index the graph moved, naming what the sync re-indexed — the live refresh The server WATCHES and never syncs: the project tree through the engine's own FileWatcher with a notify-only syncFn, the index through one non-recursive fs.watch on the data directory settled at 400 ms. Both start with the first subscriber and stop with the last, so a viewer nobody has open costs no watch descriptors. Nothing polls, on either side. Drift is now parity with codegraph_node (#1474) rather than an absence. `/api/source?ondrift=current` serves a drifted file's CURRENT bytes flagged `showing: 'current'`, and the three screens that can say so switch off everything anchored to the old line numbering — gutter ports, call-site links, call arcs, the callee rail's anchoring — while keeping the source. The banner is paper-2 with a hairline rule, never amber: amber belongs to the untested badge. Also fixes a stale read this exposed. A long-lived reader holds an LRU of nodes by id that only its own writes invalidate, so `/api/node/<id>` kept answering with a symbol another process's sync had deleted while `/api/search` beside it said it was gone. GraphSession now drops the read caches when the database (or its WAL) has been written, and the Symbol view follows a symbol whose id changed because an edit above it moved its start line, carrying the trail across. Measured on a live viewer: banner 360 ms after a save, toast 440 ms after `codegraph sync` returns, 0 requests in 4 idle seconds, and the client gives up reconnecting after ~90 s with "Not live" rather than hammering a dead port.
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
import FlowCard from '../components/flow/FlowCard.svelte';
|
||||
import FlowLink from '../components/flow/FlowLink.svelte';
|
||||
import { fetchFlow, type WireFlow, type WireFlowPayload } from '../lib/api';
|
||||
import { live } from '../lib/live.svelte';
|
||||
import { navigate, symbolHref } from '../lib/router.svelte';
|
||||
import { trail, encodeTrail, type TrailHop } from '../lib/trail.svelte';
|
||||
import { decodeTrail } from '../lib/trail-codec';
|
||||
@@ -72,14 +73,19 @@
|
||||
error = null;
|
||||
return;
|
||||
}
|
||||
// Re-run when the index moves: a path is a walk over edges that a sync can
|
||||
// add, remove or re-route, and a strip drawn from the previous graph would
|
||||
// disagree with `codegraph_explore` about the same question.
|
||||
void live.indexTick;
|
||||
const controller = new AbortController();
|
||||
loading = true;
|
||||
error = null;
|
||||
const keep = picked;
|
||||
fetchFlow(spec, controller.signal)
|
||||
.then((next) => {
|
||||
payload = next;
|
||||
picked = next.flows[0]?.id ?? null;
|
||||
showAll = false;
|
||||
// A refresh keeps the reader's chosen path when it survived the sync.
|
||||
picked = next.flows.some((f) => f.id === keep) ? keep : (next.flows[0]?.id ?? null);
|
||||
loading = false;
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
|
||||
Reference in New Issue
Block a user