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:
+26
-3
@@ -145,13 +145,20 @@ export interface WireSource {
|
||||
file: string;
|
||||
language: string;
|
||||
drift: boolean;
|
||||
/**
|
||||
* Which numbering `lines` belong to. `'indexed'` — the file matches the
|
||||
* index. `'current'` — it drifted and we asked for the bytes anyway
|
||||
* (`ondrift: 'current'`), so nothing the graph holds about this file lines up
|
||||
* with them. `'none'` — it drifted and no slice came back.
|
||||
*/
|
||||
showing: 'indexed' | 'current' | 'none';
|
||||
contentHash: string;
|
||||
indexedAt: number;
|
||||
generated: boolean;
|
||||
totalLines: number | null;
|
||||
from?: number;
|
||||
to?: number;
|
||||
/** Absent when `drift` — a mis-sliced body is worse than no body. */
|
||||
/** Absent when the file drifted and `ondrift` was left at its default. */
|
||||
lines?: string[];
|
||||
truncated?: boolean;
|
||||
reason?: string;
|
||||
@@ -590,13 +597,29 @@ export function fetchFileCode(
|
||||
return getJson<WireFileCodePayload>(`api/filecode/${encoded}`, signal);
|
||||
}
|
||||
|
||||
/**
|
||||
* A slice of an indexed file.
|
||||
*
|
||||
* `ondrift` decides what happens when the file has changed since it was
|
||||
* indexed. The default omits the slice — an indexed range over rewritten bytes
|
||||
* can show a different symbol's code under the right name. `'current'` asks for
|
||||
* the file's current lines instead, which is only correct for a caller that is
|
||||
* also going to SAY so: the response comes back `showing: 'current'`, and every
|
||||
* line-anchored thing the graph knows (ports, arcs, call sites, rail rows) has
|
||||
* to be switched off over it.
|
||||
*/
|
||||
export function fetchSource(
|
||||
file: string,
|
||||
from: number,
|
||||
to: number,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
ondrift?: 'current'
|
||||
): Promise<WireSource> {
|
||||
const params = new URLSearchParams({ file, from: String(from), to: String(to) });
|
||||
const params = new URLSearchParams({ file, from: String(from) });
|
||||
// `to` is 1-based on the wire and absent means "to the end of the file" —
|
||||
// sending 0 for that would be out of range, not a synonym.
|
||||
if (to > 0) params.set('to', String(to));
|
||||
if (ondrift) params.set('ondrift', ondrift);
|
||||
return getJson<WireSource>(`api/source?${params}`, signal);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user